diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 7e09f9b4d..24dc569d8 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -114,6 +114,11 @@ namespace MWBase /// references that are currently not in the scene should be ignored. virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0; + + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently + /// paused we may want to do it manually (after equipping permanent enchantment) + virtual void updateMagicEffects (const MWWorld::Ptr& ptr) = 0; + }; } diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index fd24dd93f..5c67f17af 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -20,6 +20,7 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/movement.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" @@ -467,12 +468,9 @@ namespace MWClass else { weapon.getCellRef().mEnchantmentCharge -= castCost; - // Touch - othercls.getCreatureStats(victim).getActiveSpells().addSpell(enchantmentName, victim, ptr, ESM::RT_Touch, weapon.getClass().getName(weapon)); - // Self - getCreatureStats(ptr).getActiveSpells().addSpell(enchantmentName, ptr, ptr, ESM::RT_Self, weapon.getClass().getName(weapon)); - // Target - MWBase::Environment::get().getWorld()->launchProjectile(enchantmentName, enchantment->mEffects, ptr, weapon.getClass().getName(weapon)); + + MWMechanics::CastSpell cast(ptr, victim); + cast.cast(weapon); } } } @@ -932,11 +930,8 @@ namespace MWClass bool Npc::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const { - MWMechanics::CreatureStats& stats = getCreatureStats (ptr); - - /// \todo consider instant effects - - return stats.getActiveSpells().addSpell (id, actor, actor); + MWMechanics::CastSpell cast(ptr, ptr); + return cast.cast(id); } void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index a18e88f5f..e93e96c4b 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -21,17 +21,17 @@ namespace MWGui { - void EffectSourceVisitor::visit (const ESM::ENAMstruct& enam, + void EffectSourceVisitor::visit (MWMechanics::EffectKey key, const std::string& sourceName, float magnitude, float remainingTime) { MagicEffectInfo newEffectSource; - newEffectSource.mKey = MWMechanics::EffectKey(enam); + newEffectSource.mKey = key; newEffectSource.mMagnitude = magnitude; newEffectSource.mPermanent = mIsPermanent; newEffectSource.mRemainingTime = remainingTime; newEffectSource.mSource = sourceName; - mEffectSources[enam.mEffectID].push_back(newEffectSource); + mEffectSources[key.mId].push_back(newEffectSource); } diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index bae108a1d..a29e2a00a 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -42,7 +42,7 @@ namespace MWGui std::map > mEffectSources; - virtual void visit (const ESM::ENAMstruct& enam, + virtual void visit (MWMechanics::EffectKey key, const std::string& sourceName, float magnitude, float remainingTime = -1); }; diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 89543476b..dc79901b0 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -1,31 +1,7 @@ - #include "activespells.hpp" -#include - -#include - -#include -#include -#include -#include -#include - -#include "../mwworld/esmstore.hpp" - #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwbase/windowmanager.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" - -#include "../mwmechanics/spellcasting.hpp" - -#include "creaturestats.hpp" -#include "npcstats.hpp" namespace MWMechanics { @@ -35,6 +11,7 @@ namespace MWMechanics MWWorld::TimeStamp now = MWBase::Environment::get().getWorld()->getTimeStamp(); + // Erase no longer active spells if (mLastUpdate!=now) { TContainer::iterator iter (mSpells.begin()); @@ -42,7 +19,6 @@ namespace MWMechanics if (!timeToExpire (iter)) { mSpells.erase (iter++); - //onSpellExpired rebuild = true; } else @@ -69,277 +45,28 @@ namespace MWMechanics for (TIterator iter (begin()); iter!=end(); ++iter) { - std::pair > effects = getEffectList (iter->first); - const MWWorld::TimeStamp& start = iter->second.mTimeStamp; - int i = 0; - for (std::vector::const_iterator effectIter (effects.first.mList.begin()); - effectIter!=effects.first.mList.end(); ++effectIter, ++i) + const std::vector& effects = iter->second.mEffects; + + for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) { - float random = iter->second.mRandom[i]; - if (effectIter->mRange != iter->second.mRange) - continue; + int duration = effectIt->mDuration; + MWWorld::TimeStamp end = start; + end += static_cast (duration)* + MWBase::Environment::get().getWorld()->getTimeScaleFactor()/(60*60); - if (effectIter->mDuration) - { - int duration = effectIter->mDuration; - - if (effects.second.first) - duration *= random; - - MWWorld::TimeStamp end = start; - end += static_cast (duration)* - MWBase::Environment::get().getWorld()->getTimeScaleFactor()/(60*60); - - if (end>now) - { - EffectParam param; - - if (effects.second.first) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effectIter->mEffectID); - - if (effectIter->mDuration==0) - { - param.mMagnitude = - static_cast (random / (0.1 * magicEffect->mData.mBaseCost)); - } - else - { - param.mMagnitude = - static_cast (0.05*random / (0.1 * magicEffect->mData.mBaseCost)); - } - } - else - param.mMagnitude = static_cast ( - (effectIter->mMagnMax-effectIter->mMagnMin)*random + effectIter->mMagnMin); - param.mMagnitude *= iter->second.mMultiplier[i]; - - if (param.mMagnitude) - mEffects.add (*effectIter, param); - } - } + if (end>now) + mEffects.add(effectIt->mKey, MWMechanics::EffectParam(effectIt->mMagnitude)); } } } - std::pair > ActiveSpells::getEffectList (const std::string& id) const - { - if (const ESM::Enchantment* enchantment = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - return std::make_pair (enchantment->mEffects, std::make_pair(false, false)); - - if (const ESM::Spell *spell = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - return std::make_pair (spell->mEffects, std::make_pair(false, false)); - - if (const ESM::Potion *potion = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - return std::make_pair (potion->mEffects, std::make_pair(false, true)); - - if (const ESM::Ingredient *ingredient = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - ingredient->mData.mEffectID[0]); - - ESM::ENAMstruct effect; - effect.mEffectID = ingredient->mData.mEffectID[0]; - effect.mSkill = ingredient->mData.mSkills[0]; - effect.mAttribute = ingredient->mData.mAttributes[0]; - effect.mRange = 0; - effect.mArea = 0; - effect.mDuration = magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration ? 0 : 1; - effect.mMagnMin = 1; - effect.mMagnMax = 1; - - std::pair > result; - result.second.second = true; - result.second.first = true; - - result.first.mList.push_back (effect); - - return result; - } - - throw std::runtime_error ("ID " + id + " can not produce lasting effects"); - } - - std::string ActiveSpells::getSpellDisplayName (const std::string& id) const - { - if (const ESM::Spell *spell = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - return spell->mName; - - if (const ESM::Potion *potion = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - return potion->mName; - - if (const ESM::Ingredient *ingredient = - MWBase::Environment::get().getWorld()->getStore().get().search (id)) - return ingredient->mName; - - throw std::runtime_error ("ID " + id + " has no display name"); - } - ActiveSpells::ActiveSpells() - : mSpellsChanged (false), mLastUpdate (MWBase::Environment::get().getWorld()->getTimeStamp()) + : mSpellsChanged (false) + , mLastUpdate (MWBase::Environment::get().getWorld()->getTimeStamp()) {} - bool ActiveSpells::addSpell (const std::string& id, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, ESM::RangeType range, const std::string& name, int effectIndex) - { - const CreatureStats& creatureStats = MWWorld::Class::get (actor).getCreatureStats (actor); - - std::pair > effects = getEffectList (id); - bool stacks = effects.second.second; - - bool found = false; - - for (std::vector::const_iterator iter (effects.first.mList.begin()); - iter!=effects.first.mList.end(); ++iter) - { - if (iter->mRange != range) - continue; - if (iter->mDuration) - { - found = true; - break; - } - } - - // If none of the effects need to apply, no need to add the spell - if (!found) - return false; - - TContainer::iterator iter = mSpells.find (id); - - ActiveSpellParams params; - for (unsigned int i=0; i (std::rand()) / RAND_MAX; - if (effects.second.first) - { - // ingredient -> special treatment required. - const NpcStats& npcStats = MWWorld::Class::get (actor).getNpcStats (actor); - - float x = - (npcStats.getSkill (ESM::Skill::Alchemy).getModified() + - 0.2 * creatureStats.getAttribute (1).getModified() - + 0.1 * creatureStats.getAttribute (7).getModified()) - * creatureStats.getFatigueTerm(); - random *= 100; - random = random / std::min (x, 100.0f); - random *= 0.25 * x; - } - - params.mRandom.push_back(random); - } - params.mRange = range; - params.mTimeStamp = MWBase::Environment::get().getWorld()->getTimeStamp(); - params.mName = name; - params.mMultiplier.resize(effects.first.mList.size(), 1); - - /* - for (int i=0; imRange != range) - { - params.mDisabled.push_back(true); - continue; - } - - bool disabled = false; - - int reflect = creatureStats.getMagicEffects().get(ESM::MagicEffect::Reflect).mMagnitude; - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (roll < reflect) - disabled = true; - } - */ - - bool first=true; - int i = 0; - for (std::vector::const_iterator effectIt (effects.first.mList.begin()); - effectIt!=effects.first.mList.end(); ++effectIt, ++i) - { - if (effectIt->mRange != range) - continue; - - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effectIt->mEffectID); - - if (caster.getRefData().getHandle() == "player" && actor != caster - && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) - MWBase::Environment::get().getWindowManager()->setEnemy(actor); - - // Try resisting effect in case its harmful - const ESM::Spell *spell = - MWBase::Environment::get().getWorld()->getStore().get().search (id); - params.mMultiplier[i] = MWMechanics::getEffectMultiplier(effectIt->mEffectID, actor, caster, spell); - if (params.mMultiplier[i] == 0) - { - if (actor.getRefData().getHandle() == "player") - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); - } - - // If fully resisted, don't play sounds or particles - if (params.mMultiplier[i] == 0) - continue; - - // TODO: For Area effects, launch a growing particle effect that applies the effect to more actors as it hits them. Best managed in World. - - // Only the sound of the first effect plays - if (first) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(actor, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(actor, 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; - MWBase::Environment::get().getWorld()->getAnimation(actor)->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, ""); - } - - first = false; - } - - if (iter==mSpells.end() || stacks) - mSpells.insert (std::make_pair (id, params)); - else - iter->second = params; - - mSpellsChanged = true; - - return true; - } - - void ActiveSpells::removeSpell (const std::string& id) - { - TContainer::iterator iter = mSpells.find (id); - - if (iter!=mSpells.end()) - { - mSpells.erase (iter); - mSpellsChanged = true; - } - } - const MagicEffects& ActiveSpells::getMagicEffects() const { update(); @@ -348,37 +75,31 @@ namespace MWMechanics ActiveSpells::TIterator ActiveSpells::begin() const { - update(); return mSpells.begin(); } ActiveSpells::TIterator ActiveSpells::end() const { - update(); return mSpells.end(); } double ActiveSpells::timeToExpire (const TIterator& iterator) const { - std::pair > effects = getEffectList (iterator->first); + const std::vector& effects = iterator->second.mEffects; int duration = 0; - for (std::vector::const_iterator iter (effects.first.mList.begin()); - iter!=effects.first.mList.end(); ++iter) + for (std::vector::const_iterator iter (effects.begin()); + iter!=effects.end(); ++iter) { if (iter->mDuration > duration) duration = iter->mDuration; } - // Scale duration by magnitude if needed - if (effects.second.first && iterator->second.mRandom.size()) - duration *= iterator->second.mRandom.front(); - double scaledDuration = duration * MWBase::Environment::get().getWorld()->getTimeScaleFactor()/(60*60); - double usedUp = MWBase::Environment::get().getWorld()->getTimeStamp()-iterator->second.mTimeStamp; + double usedUp = MWBase::Environment::get().getWorld()->getTimeStamp() - iterator->second.mTimeStamp; if (usedUp>=scaledDuration) return 0; @@ -405,48 +126,67 @@ namespace MWMechanics return mSpells; } + void ActiveSpells::addSpell(const std::string &id, bool stack, std::vector effects, const std::string &displayName) + { + bool exists = false; + for (TContainer::const_iterator it = begin(); it != end(); ++it) + { + if (id == it->first) + exists = true; + } + + ActiveSpellParams params; + params.mTimeStamp = MWBase::Environment::get().getWorld()->getTimeStamp(); + params.mEffects = effects; + params.mDisplayName = displayName; + + if (!exists || stack) + mSpells.insert (std::make_pair(id, params)); + else + mSpells.find(id)->second = params; + + mSpellsChanged = true; + } + void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const { for (TContainer::const_iterator it = begin(); it != end(); ++it) { - const ESM::EffectList& list = getEffectList(it->first).first; - float timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); - int i=0; - for (std::vector::const_iterator effectIt = list.mList.begin(); - effectIt != list.mList.end(); ++effectIt, ++i) + for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); + effectIt != it->second.mEffects.end(); ++effectIt) { - if (effectIt->mRange != it->second.mRange) - continue; - - std::string name; - if (it->second.mName.empty()) - name = getSpellDisplayName(it->first); - else - name = it->second.mName; + std::string name = it->second.mDisplayName; float remainingTime = effectIt->mDuration + (it->second.mTimeStamp - MWBase::Environment::get().getWorld()->getTimeStamp())*3600/timeScale; - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * it->second.mRandom[i]; + float magnitude = effectIt->mMagnitude; - // hack for ingredients - if (MWBase::Environment::get().getWorld()->getStore().get().search (it->first)) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effectIt->mEffectID); - - remainingTime = effectIt->mDuration * it->second.mRandom[i] + - (it->second.mTimeStamp - MWBase::Environment::get().getWorld()->getTimeStamp())*3600/timeScale; - - magnitude = static_cast (0.05*it->second.mRandom[i] / (0.1 * magicEffect->mData.mBaseCost)); - } - - magnitude *= it->second.mMultiplier[i]; if (magnitude) - visitor.visit(*effectIt, name, magnitude, remainingTime); + visitor.visit(effectIt->mKey, name, magnitude, remainingTime); } } } + + void ActiveSpells::purgeAll() + { + mSpells.clear(); + } + + void ActiveSpells::purgeEffect(short effectId) + { + for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + { + for (std::vector::iterator effectIt = it->second.mEffects.begin(); + effectIt != it->second.mEffects.end();) + { + if (effectIt->mKey.mId == effectId) + effectIt = it->second.mEffects.erase(effectIt); + else + effectIt++; + } + } + + } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 56d9413c3..001402337 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -11,39 +11,8 @@ #include -namespace ESM -{ - struct Spell; - struct EffectList; -} - -namespace MWWorld -{ - class Ptr; -} - namespace MWMechanics { - struct ActiveSpellParams - { - // Only apply effects of this range type - ESM::RangeType mRange; - - // When the spell was added - MWWorld::TimeStamp mTimeStamp; - - // Random factor for each effect - std::vector mRandom; - - // Effect magnitude multiplier. Use 0 to completely disable the effect - // (if it was resisted, reflected or absorbed). Use (0,1) for partially resisted. - std::vector mMultiplier; - - // Display name, we need this for enchantments, which don't have a name - so you need to supply the - // name of the item with the enchantment to addSpell - std::string mName; - }; - /// \brief Lasting spell effects /// /// \note The name of this class is slightly misleading, since it also handels lasting potion @@ -52,6 +21,23 @@ namespace MWMechanics { public: + // 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 Effect + { + float mMagnitude; + EffectKey mKey; + float mDuration; + }; + + struct ActiveSpellParams + { + std::vector mEffects; + MWWorld::TimeStamp mTimeStamp; + std::string mDisplayName; + }; + typedef std::multimap TContainer; typedef TContainer::const_iterator TIterator; @@ -66,9 +52,6 @@ namespace MWMechanics void rebuildEffects() const; - std::pair > getEffectList (const std::string& id) const; - ///< @return (EffectList, (isIngredient, stacks)) - double timeToExpire (const TIterator& iterator) const; ///< Returns time (in in-game hours) until the spell pointed to by \a iterator /// expires. @@ -79,25 +62,25 @@ namespace MWMechanics TIterator end() const; - std::string getSpellDisplayName (const std::string& id) const; - public: ActiveSpells(); - bool addSpell (const std::string& id, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, ESM::RangeType range = ESM::RT_Self, const std::string& name = "", int effectIndex = -1); - ///< Overwrites an existing spell with the same ID. If the spell does not have any - /// non-instant effects, it is ignored. - /// @param id - /// @param actor actor to add the spell to - /// @param caster actor who casted the spell - /// @param range Only effects with range type \a range will be applied - /// @param name Display name for enchantments, since they don't have a name in their record - /// @param effectIndex Only apply one specific effect - useful for reflecting spells, since each effect is reflected individually + /// Add lasting effects /// - /// \return Has the spell been added? + /// \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, std::vector effects, const std::string& displayName); - void removeSpell (const std::string& id); + /// Remove all active effects with this id + void purgeEffect (short effectId); + + /// Remove all active effects + void purgeAll (); bool isSpellActive (std::string id) const; ///< case insensitive diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 251c38ec0..01a96f199 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -53,6 +53,10 @@ namespace MWMechanics Actors(); + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently + /// paused we may want to do it manually (after equipping permanent enchantment) + void updateMagicEffects (const MWWorld::Ptr& ptr) { adjustMagicEffects(ptr); } + void addActor (const MWWorld::Ptr& ptr); ///< Register an actor for stats management /// diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 7df6ec566..c7a6b3875 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -516,6 +516,12 @@ bool CharacterController::updateNpcState(bool onground, bool inwater, bool isrun const ESM::Static* castStatic = store.get().find (effect->mCasting); mAnimation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex); + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); + //mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); + //mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Left Hand", effect->mParticle); + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Right Hand", effect->mParticle); + switch(effectentry.mRange) { case 0: mAttackType = "self"; break; diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 58f023eba..2c1b363b7 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -12,13 +12,6 @@ namespace ESM namespace MWMechanics { - // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display - struct EffectSourceVisitor - { - virtual void visit (const ESM::ENAMstruct& enam, - const std::string& sourceName, float magnitude, float remainingTime = -1) = 0; - }; - struct EffectKey { int mId; @@ -59,6 +52,13 @@ namespace MWMechanics return param -= right; } + // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display + struct EffectSourceVisitor + { + virtual void visit (MWMechanics::EffectKey key, + const std::string& sourceName, float magnitude, float remainingTime = -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 29b12b0f3..e1a7ac123 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -679,4 +679,9 @@ namespace MWMechanics return false; } + void MechanicsManager::updateMagicEffects(const MWWorld::Ptr &ptr) + { + mActors.updateMagicEffects(ptr); + } + } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index aedb84b29..42656d5ab 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -100,6 +100,11 @@ namespace MWMechanics virtual void playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number); virtual void skipAnimation(const MWWorld::Ptr& ptr); virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName); + + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently + /// paused we may want to do it manually (after equipping permanent enchantment) + virtual void updateMagicEffects (const MWWorld::Ptr& ptr); + }; } diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp new file mode 100644 index 000000000..00ca82c5a --- /dev/null +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -0,0 +1,473 @@ +#include "spellcasting.hpp" + +#include + +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/soundmanager.hpp" + + +#include "../mwworld/containerstore.hpp" + +#include "../mwrender/animation.hpp" + +namespace MWMechanics +{ + + CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target) + : mCaster(caster) + , mTarget(target) + , mStack(false) + { + } + + void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, + const ESM::EffectList &effects, ESM::RangeType range, bool reflected) + { + // If none of the effects need to apply, we can early-out + bool found = false; + for (std::vector::const_iterator iter (effects.mList.begin()); + iter!=effects.mList.end(); ++iter) + { + if (iter->mRange != range) + continue; + found = true; + } + if (!found) + return; + + ESM::EffectList reflectedEffects; + std::vector appliedLastingEffects; + bool firstAppliedEffect = true; + + for (std::vector::const_iterator effectIt (effects.mList.begin()); + effectIt!=effects.mList.end(); ++effectIt) + { + if (effectIt->mRange != range) + continue; + + const ESM::MagicEffect *magicEffect = + MWBase::Environment::get().getWorld()->getStore().get().find ( + effectIt->mEffectID); + + float magnitudeMult = 1; + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && target.getClass().isActor()) + { + // If player is attempting to cast a harmful spell, show the target's HP bar + if (caster.getRefData().getHandle() == "player" && target != caster) + MWBase::Environment::get().getWindowManager()->setEnemy(target); + + // Try absorbing if it's a spell + // NOTE: Vanilla does this once per effect source instead of adding the % from all sources together, not sure + // if that is worth replicating. + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search (mId)) + { + int absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).mMagnitude; + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + bool isAbsorbed = (roll < absorb); + if (isAbsorbed) + { + const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Absorb"); + MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect( + "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::Reflect, false, ""); + // Magicka is increased by cost of spell + DynamicStat magicka = target.getClass().getCreatureStats(target).getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost); + target.getClass().getCreatureStats(target).setMagicka(magicka); + magnitudeMult = 0; + } + } + + // Try reflecting + if (!reflected && magnitudeMult > 0) + { + int reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).mMagnitude; + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + bool isReflected = (roll < reflect); + if (isReflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); + MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect( + "meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, ""); + reflectedEffects.mList.push_back(*effectIt); + magnitudeMult = 0; + } + } + + // Try resisting + if (magnitudeMult > 0 && caster.getClass().isActor()) + { + const ESM::Spell *spell = + MWBase::Environment::get().getWorld()->getStore().get().search (mId); + magnitudeMult = MWMechanics::getEffectMultiplier(effectIt->mEffectID, target, caster, spell); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + } + } + } + + + if (magnitudeMult > 0) + { + float random = std::rand() / static_cast(RAND_MAX); + float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; + magnitude *= magnitudeMult; + + if (target.getClass().isActor() && !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + { + ActiveSpells::Effect effect; + effect.mKey = MWMechanics::EffectKey(*effectIt); + effect.mDuration = effectIt->mDuration; + effect.mMagnitude = magnitude; + + appliedLastingEffects.push_back(effect); + } + else + applyInstantEffect(mTarget, effectIt->mEffectID, magnitude); + + if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + { + // Play sound, only for the first effect + if (firstAppliedEffect) + { + 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); + firstAppliedEffect = false; + } + + // Add VFX + 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; + // 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) + anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, ""); + } + } + + // TODO: For Area effects, launch a growing particle effect that applies the effect to more actors as it hits them. Best managed in World. + } + } + + if (reflectedEffects.mList.size()) + inflict(caster, target, reflectedEffects, range, true); + + if (appliedLastingEffects.size()) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, mSourceName); + } + + void CastSpell::applyInstantEffect(const MWWorld::Ptr &target, short effectId, float magnitude) + { + if (!target.getClass().isActor()) + { + if (effectId == ESM::MagicEffect::Lock) + { + if (target.getCellRef().mLockLevel < magnitude) + target.getCellRef().mLockLevel = magnitude; + } + else if (effectId == ESM::MagicEffect::Open) + { + // TODO: This is a crime + if (target.getCellRef().mLockLevel <= magnitude) + { + if (target.getCellRef().mLockLevel > 0) + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); + target.getCellRef().mLockLevel = 0; + } + else + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + } + } + else + { + if (effectId == ESM::MagicEffect::CurePoison) + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); + else if (effectId == ESM::MagicEffect::CureParalyzation) + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); + else if (effectId == ESM::MagicEffect::CureCommonDisease) + target.getClass().getCreatureStats(target).getSpells().purgeCommonDisease(); + else if (effectId == ESM::MagicEffect::CureBlightDisease) + target.getClass().getCreatureStats(target).getSpells().purgeBlightDisease(); + else if (effectId == ESM::MagicEffect::CureCorprusDisease) + target.getClass().getCreatureStats(target).getSpells().purgeCorprusDisease(); + else if (effectId == ESM::MagicEffect::Dispel) + target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(); + else if (effectId == ESM::MagicEffect::RemoveCurse) + target.getClass().getCreatureStats(target).getSpells().purgeCurses(); + + else if (effectId == ESM::MagicEffect::DivineIntervention) + { + // We need to be able to get the world location of an interior cell before implementing this + // or alternatively, the last known exterior location of the player, which is how vanilla does it. + } + else if (effectId == ESM::MagicEffect::AlmsiviIntervention) + { + // Same as above + } + + else if (effectId == ESM::MagicEffect::Mark) + { + // TODO + } + else if (effectId == ESM::MagicEffect::Recall) + { + // TODO + } + } + } + + bool CastSpell::cast(const std::string &id) + { + if (const ESM::Spell *spell = + MWBase::Environment::get().getWorld()->getStore().get().search (id)) + return cast(spell); + + if (const ESM::Potion *potion = + MWBase::Environment::get().getWorld()->getStore().get().search (id)) + return cast(potion); + + if (const ESM::Ingredient *ingredient = + MWBase::Environment::get().getWorld()->getStore().get().search (id)) + return cast(ingredient); + + throw std::runtime_error("ID type cannot be casted"); + } + + bool CastSpell::cast(const MWWorld::Ptr &item) + { + std::string enchantmentName = item.getClass().getEnchantment(item); + if (enchantmentName.empty()) + throw std::runtime_error("can't cast an item without an enchantment"); + + mSourceName = item.getClass().getName(item); + mId = item.getCellRef().mRefID; + + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); + + mStack = (enchantment->mData.mType == ESM::Enchantment::CastOnce); + + if (enchantment->mData.mType == ESM::Enchantment::WhenUsed) + { + // Check if there's enough charge left + const float enchantCost = enchantment->mData.mCost; + MWMechanics::NpcStats &stats = MWWorld::Class::get(mCaster).getNpcStats(mCaster); + int eSkill = stats.getSkill(ESM::Skill::Enchant).getModified(); + const float castCost = enchantCost - (enchantCost / 100) * (eSkill - 10); + + if (item.getCellRef().mEnchantmentCharge == -1) + item.getCellRef().mEnchantmentCharge = enchantment->mData.mCharge; + + if (mCaster.getRefData().getHandle() == "player" && item.getCellRef().mEnchantmentCharge < castCost) + { + // TODO: Should there be a sound here? + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); + return false; + } + + // Reduce charge + item.getCellRef().mEnchantmentCharge -= castCost; + } + if (enchantment->mData.mType == ESM::Enchantment::CastOnce) + item.getContainerStore()->remove(item, 1, mCaster); + else + { + if (mCaster.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(item); // Set again to show the modified charge + } + + inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); + + if (!mTarget.isEmpty()) + { + if (!mTarget.getClass().isActor() || !mTarget.getClass().getCreatureStats(mTarget).isDead()) + inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); + } + + MWBase::Environment::get().getWorld()->launchProjectile(mId, enchantment->mEffects, mCaster, mSourceName); + + return true; + } + + bool CastSpell::cast(const ESM::Potion* potion) + { + mSourceName = potion->mName; + mId = potion->mId; + mStack = true; + + inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); + + return true; + } + + bool CastSpell::cast(const ESM::Spell* spell) + { + mSourceName = spell->mName; + mId = spell->mId; + mStack = false; + + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + + int school = 0; + + if (mCaster.getClass().isActor()) + { + school = getSpellSchool(spell, mCaster); + + CreatureStats& stats = mCaster.getClass().getCreatureStats(mCaster); + + // Reduce fatigue (note that in the vanilla game, both GMSTs are 0, and there's no fatigue loss) + static const float fFatigueSpellBase = store.get().find("fFatigueSpellBase")->getFloat(); + static const float fFatigueSpellMult = store.get().find("fFatigueSpellMult")->getFloat(); + DynamicStat fatigue = stats.getFatigue(); + const float normalizedEncumbrance = mCaster.getClass().getEncumbrance(mCaster) / mCaster.getClass().getCapacity(mCaster); + float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); + fatigue.setCurrent(std::max(0.f, fatigue.getCurrent() - fatigueLoss)); + stats.setFatigue(fatigue); + + // Check mana + bool fail = false; + DynamicStat magicka = stats.getMagicka(); + if (magicka.getCurrent() < spell->mData.mCost) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientSP}"); + fail = true; + } + + // Reduce mana + if (!fail) + { + magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); + stats.setMagicka(magicka); + } + + // If this is a power, check if it was already used in last 24h + if (!fail && spell->mData.mType & ESM::Spell::ST_Power) + { + if (stats.canUsePower(spell->mId)) + stats.usePower(spell->mId); + else + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sPowerAlreadyUsed}"); + fail = true; + } + } + + // Check success + int successChance = getSpellSuccessChance(spell, mCaster); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (!fail && roll >= successChance) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); + fail = true; + } + + if (fail) + { + // Failure sound + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); + return false; + } + } + + if (mCaster.getRefData().getHandle() == "player" && spell->mData.mType == ESM::Spell::ST_Spell) + mCaster.getClass().skillUsageSucceeded(mCaster, + spellSchoolToSkill(school), 0); + + inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self); + + if (!mTarget.isEmpty()) + { + if (!mTarget.getClass().isActor() || !mTarget.getClass().getCreatureStats(mTarget).isDead()) + { + inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); + } + } + + MWBase::Environment::get().getWorld()->launchProjectile(mId, spell->mEffects, mCaster, mSourceName); + return true; + } + + bool CastSpell::cast (const ESM::Ingredient* ingredient) + { + mId = ingredient->mId; + mStack = true; + mSourceName = ingredient->mName; + + ESM::ENAMstruct effect; + effect.mEffectID = ingredient->mData.mEffectID[0]; + effect.mSkill = ingredient->mData.mSkills[0]; + effect.mAttribute = ingredient->mData.mAttributes[0]; + effect.mRange = ESM::RT_Self; + effect.mArea = 0; + + const ESM::MagicEffect *magicEffect = + MWBase::Environment::get().getWorld()->getStore().get().find ( + effect.mEffectID); + + const MWMechanics::NpcStats& npcStats = mCaster.getClass().getNpcStats(mCaster); + const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster); + + float x = (npcStats.getSkill (ESM::Skill::Alchemy).getModified() + + 0.2 * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() + + 0.1 * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()) + * creatureStats.getFatigueTerm(); + + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (roll > x) + { + // "X has no effect on you" + std::string message = MWBase::Environment::get().getWorld()->getStore().get().find("#{sNotifyMessage50}")->getString(); + message = boost::str(boost::format(message) % ingredient->mName); + + MWBase::Environment::get().getWindowManager()->messageBox(message); + return false; + } + + float magnitude = 0; + float y = roll / std::min(x, 100.f); + y *= 0.25 * x; + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + effect.mDuration = int(y); + else + effect.mDuration = 1; + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + if (!magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + magnitude = int((0.05 * y) / (0.1 * magicEffect->mData.mBaseCost)); + else + magnitude = int(y / (0.1 * magicEffect->mData.mBaseCost)); + magnitude = std::max(1.f, magnitude); + } + else + magnitude = 1; + + effect.mMagnMax = magnitude; + effect.mMagnMin = magnitude; + + ESM::EffectList effects; + effects.mList.push_back(effect); + + inflict(mCaster, mCaster, effects, ESM::RT_Self); + + return true; + } + +} diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 3bbb4d0c5..5e87e1993 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -44,12 +44,6 @@ namespace MWMechanics if (creatureStats.getMagicEffects().get(ESM::MagicEffect::Silence).mMagnitude) return 0; - if (spell->mData.mType != ESM::Spell::ST_Spell) - return 100; - - if (spell->mData.mFlags & ESM::Spell::F_Always) - return 100; - float y = FLT_MAX; float lowestSkill = 0; @@ -79,8 +73,14 @@ namespace MWMechanics } } + if (spell->mData.mType != ESM::Spell::ST_Spell) + return 100; + + if (spell->mData.mFlags & ESM::Spell::F_Always) + return 100; int castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).mMagnitude; + int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); @@ -98,7 +98,6 @@ namespace MWMechanics return getSpellSuccessChance(spell, actor, effectiveSchool); } - /// @note this only works for ST_Spell inline int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor) { int school = 0; @@ -106,7 +105,6 @@ namespace MWMechanics return school; } - /// @note this only works for ST_Spell inline int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) { int school = 0; @@ -180,6 +178,34 @@ namespace MWMechanics return -(resistance-100) / 100.f; } + + class CastSpell + { + private: + MWWorld::Ptr mCaster; + MWWorld::Ptr mTarget; + + bool mStack; + std::string mId; // ID of spell, potion, item etc + std::string mSourceName; // Display name for spell, potion, etc + + public: + CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target); + + bool cast (const ESM::Spell* spell); + bool cast (const MWWorld::Ptr& item); + bool cast (const ESM::Ingredient* ingredient); + bool cast (const ESM::Potion* potion); + + /// @note Auto detects if spell, ingredient or potion + bool cast (const std::string& id); + + void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false); + + void applyInstantEffect (const MWWorld::Ptr& target, short effectId, float magnitude); + }; + } #endif diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index a5a5677d1..5b18e2a3c 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -118,6 +118,62 @@ namespace MWMechanics return false; } + void Spells::purgeCommonDisease() + { + for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + { + const ESM::Spell *spell = + MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); + + if (spell->mData.mType & ESM::Spell::ST_Disease) + mSpells.erase(iter++); + else + iter++; + } + } + + void Spells::purgeBlightDisease() + { + for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + { + const ESM::Spell *spell = + MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); + + if (spell->mData.mType & ESM::Spell::ST_Blight) + mSpells.erase(iter++); + else + iter++; + } + } + + void Spells::purgeCorprusDisease() + { + for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + { + const ESM::Spell *spell = + MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); + + if (Misc::StringUtils::ciEqual(spell->mId, "corprus")) + mSpells.erase(iter++); + else + iter++; + } + } + + void Spells::purgeCurses() + { + for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + { + const ESM::Spell *spell = + MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); + + if (spell->mData.mType == ESM::Spell::ST_Curse) + mSpells.erase(iter++); + else + iter++; + } + } + void Spells::visitEffectSources(EffectSourceVisitor &visitor) const { for (TIterator it = begin(); it != end(); ++it) @@ -136,7 +192,7 @@ namespace MWMechanics effectIt != list.mList.end(); ++effectIt, ++i) { float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * it->second[i]; - visitor.visit(*effectIt, spell->mName, magnitude); + visitor.visit(MWMechanics::EffectKey(*effectIt), spell->mName, magnitude); } } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 79b7a782d..cf9b66091 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -35,6 +35,11 @@ namespace MWMechanics public: + void purgeCommonDisease(); + void purgeBlightDisease(); + void purgeCorprusDisease(); + void purgeCurses(); + TIterator begin() const; TIterator end() const; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 10c925b36..a29beb35c 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -994,16 +994,27 @@ void Animation::detachObjectFromBone(Ogre::MovableObject *obj) mSkelBase->detachObjectFromBone(obj); } -void Animation::addEffect(const std::string &model, int effectId, bool loop, const std::string &bonename) +void Animation::addEffect(const std::string &model, int effectId, bool loop, const std::string &bonename, std::string texture) { // Early out if we already have this effect for (std::vector::iterator it = mEffects.begin(); it != mEffects.end(); ++it) if (it->mLoop && loop && it->mEffectId == effectId && it->mBoneName == bonename) return; + // fix texture extension to .dds + if (texture.size() > 4) + { + texture[texture.size()-3] = 'd'; + texture[texture.size()-2] = 'd'; + texture[texture.size()-1] = 's'; + } + EffectParams params; params.mModelName = model; - params.mObjects = NifOgre::Loader::createObjects(mInsert, model); + if (bonename.empty()) + params.mObjects = NifOgre::Loader::createObjects(mInsert, model); + else + params.mObjects = NifOgre::Loader::createObjects(mSkelBase, bonename, mInsert, model); params.mLoop = loop; params.mEffectId = effectId; params.mBoneName = bonename; @@ -1013,6 +1024,35 @@ void Animation::addEffect(const std::string &model, int effectId, bool loop, con if(params.mObjects.mControllers[i].getSource().isNull()) params.mObjects.mControllers[i].setSource(Ogre::SharedPtr (new EffectAnimationValue())); } + + if (!texture.empty()) + { + for(size_t i = 0;i < params.mObjects.mParticles.size(); ++i) + { + Ogre::ParticleSystem* partSys = params.mObjects.mParticles[i]; + Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(partSys->getMaterialName()); + static int count = 0; + Ogre::String materialName = "openmw/" + Ogre::StringConverter::toString(count++); + // TODO: destroy when effect is removed + Ogre::MaterialPtr newMat = mat->clone(materialName); + partSys->setMaterialName(materialName); + + for (int t=0; tgetNumTechniques(); ++t) + { + Ogre::Technique* tech = newMat->getTechnique(t); + for (int p=0; pgetNumPasses(); ++p) + { + Ogre::Pass* pass = tech->getPass(p); + for (int tex=0; texgetNumTextureUnitStates(); ++tex) + { + Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); + tus->setTextureName("textures\\" + texture); + } + } + } + } + } + mEffects.push_back(params); } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index b1572b6a1..7d1fd010c 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -206,9 +206,10 @@ public: * @param loop Loop the effect. If false, it is removed automatically after it finishes playing. If true, * you need to remove it manually using removeEffect when the effect should end. * @param bonename Bone to attach to, or empty string to use the scene node instead + * @param texture override the texture specified in the model's materials * @note Will not add an effect twice. */ - void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = ""); + void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", std::string texture = ""); void removeEffect (int effectId); void getLoopingEffects (std::vector& out); private: diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index fd81baf6e..4d5f6872d 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -281,3 +281,11 @@ void Objects::updateObjectCell(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) node->addChild(cur.getRefData().getBaseNode()); } +ObjectAnimation* Objects::getAnimation(const MWWorld::Ptr &ptr) +{ + PtrAnimationMap::const_iterator iter = mObjects.find(ptr); + if(iter != mObjects.end()) + return iter->second; + return NULL; +} + diff --git a/apps/openmw/mwrender/objects.hpp b/apps/openmw/mwrender/objects.hpp index 22dd1e4f5..165f6551d 100644 --- a/apps/openmw/mwrender/objects.hpp +++ b/apps/openmw/mwrender/objects.hpp @@ -41,6 +41,8 @@ public: ~Objects(){} void insertModel(const MWWorld::Ptr& ptr, const std::string &model); + ObjectAnimation* getAnimation(const MWWorld::Ptr &ptr); + void enableLights(); void disableLights(); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 57e00d76c..1b891368f 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -975,8 +975,13 @@ void RenderingManager::setupExternalRendering (MWRender::ExternalRendering& rend Animation* RenderingManager::getAnimation(const MWWorld::Ptr &ptr) { Animation *anim = mActors.getAnimation(ptr); + if(!anim && ptr.getRefData().getHandle() == "player") anim = mPlayerAnimation; + + if (!anim) + anim = mObjects.getAnimation(ptr); + return anim; } diff --git a/apps/openmw/mwworld/actioneat.cpp b/apps/openmw/mwworld/actioneat.cpp index 470eeda2b..f5d7e2636 100644 --- a/apps/openmw/mwworld/actioneat.cpp +++ b/apps/openmw/mwworld/actioneat.cpp @@ -3,17 +3,11 @@ #include -#include - #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -#include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/npcstats.hpp" - #include "../mwworld/containerstore.hpp" -#include "esmstore.hpp" #include "class.hpp" namespace MWWorld @@ -23,27 +17,11 @@ namespace MWWorld // remove used item (assume the item is present in inventory) getTarget().getContainerStore()->remove(getTarget(), 1, actor); - // check for success - const MWMechanics::CreatureStats& creatureStats = MWWorld::Class::get (actor).getCreatureStats (actor); - MWMechanics::NpcStats& npcStats = MWWorld::Class::get (actor).getNpcStats (actor); - - float x = - (npcStats.getSkill (ESM::Skill::Alchemy).getModified() + - 0.2 * creatureStats.getAttribute (1).getModified() - + 0.1 * creatureStats.getAttribute (7).getModified()) - * creatureStats.getFatigueTerm(); - - if (x>=100*static_cast (std::rand()) / RAND_MAX) - { - // apply to actor - std::string id = Class::get (getTarget()).getId (getTarget()); + // apply to actor + std::string id = Class::get (getTarget()).getId (getTarget()); - Class::get (actor).apply (actor, id, actor); - // we ignore the result here. Skill increases no matter if the ingredient did something or not. - - // increase skill + if (Class::get (actor).apply (actor, id, actor)) Class::get (actor).skillUsageSucceeded (actor, ESM::Skill::Alchemy, 1); - } } ActionEat::ActionEat (const MWWorld::Ptr& object) : Action (false, object) {} diff --git a/apps/openmw/mwworld/actiontrap.cpp b/apps/openmw/mwworld/actiontrap.cpp index 80da29072..d723b9823 100644 --- a/apps/openmw/mwworld/actiontrap.cpp +++ b/apps/openmw/mwworld/actiontrap.cpp @@ -1,26 +1,14 @@ #include "actiontrap.hpp" -#include "../mwworld/class.hpp" - -#include "../mwmechanics/activespells.hpp" -#include "../mwmechanics/creaturestats.hpp" - -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" +#include "../mwmechanics/spellcasting.hpp" namespace MWWorld { void ActionTrap::executeImp(const Ptr &actor) { - // TODO: Apply RT_Self effects on the door / container that triggered the trap. Not terribly useful, but you could - // make it lock itself when activated for example. - - actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(mSpellId, actor, actor, ESM::RT_Touch); - - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); - - MWBase::Environment::get().getWorld()->launchProjectile(mSpellId, spell->mEffects, mTrapSource, spell->mName); + MWMechanics::CastSpell cast(mTrapSource, actor); + cast.cast(mSpellId); mTrapSource.getCellRef().mTrap = ""; } diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 72523a69c..6483b32b2 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -9,6 +9,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" @@ -339,6 +340,9 @@ void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) if (params[i].mMultiplier == 0) continue; + float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; + magnitude *= params[i].mMultiplier; + if (!existed) { // During first auto equip, we don't play any sounds. @@ -346,10 +350,13 @@ void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) // the items should appear as if they'd always been equipped. mListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip, !mFirstAutoEquip && effectIt == enchantment.mEffects.mList.begin()); + + // Apply instant effects + MWMechanics::CastSpell cast(actor, actor); + if (magnitude) + cast.applyInstantEffect(actor, effectIt->mEffectID, magnitude); } - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; if (magnitude) mMagicEffects.add (*effectIt, magnitude); } @@ -376,6 +383,9 @@ void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) ++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; } @@ -442,6 +452,13 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor autoEquip(actor); } + if (item.getRefData().getCount() == 0 && mSelectedEnchantItem != end() + && *mSelectedEnchantItem == item && actor.getRefData().getHandle() == "player") + { + mSelectedEnchantItem = end(); + MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); + } + return retCount; } @@ -554,7 +571,7 @@ void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisito const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().mRefID][i]; float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params.mRandom; magnitude *= params.mMultiplier; - visitor.visit(*effectIt, (**iter).getClass().getName(**iter), magnitude); + visitor.visit(MWMechanics::EffectKey(*effectIt), (**iter).getClass().getName(**iter), magnitude); ++i; } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 2dd0a9b93..7dec848ad 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2029,156 +2029,28 @@ namespace MWWorld void World::castSpell(const Ptr &actor) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + InventoryStore& inv = actor.getClass().getInventoryStore(actor); + + // Unset casting flag, otherwise pressing the mouse button down would continue casting every frame if using an enchantment + // (which casts instantly without an animation) stats.setAttackingOrSpell(false); - ESM::EffectList effects; + MWWorld::Ptr target = getFacedObject(); std::string selectedSpell = stats.getSpells().getSelectedSpell(); - std::string sourceName; + + MWMechanics::CastSpell cast(actor, target); + if (!selectedSpell.empty()) { const ESM::Spell* spell = getStore().get().search(selectedSpell); - // Reduce fatigue (note that in the vanilla game, both GMSTs are 0, and there's no fatigue loss) - static const float fFatigueSpellBase = getStore().get().find("fFatigueSpellBase")->getFloat(); - static const float fFatigueSpellMult = getStore().get().find("fFatigueSpellMult")->getFloat(); - MWMechanics::DynamicStat fatigue = stats.getFatigue(); - const float normalizedEncumbrance = actor.getClass().getEncumbrance(actor) / actor.getClass().getCapacity(actor); - float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); - fatigue.setCurrent(std::max(0.f, fatigue.getCurrent() - fatigueLoss)); - stats.setFatigue(fatigue); - - // Check mana - bool fail = false; - MWMechanics::DynamicStat magicka = stats.getMagicka(); - if (magicka.getCurrent() < spell->mData.mCost) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientSP}"); - fail = true; - } - - // Reduce mana - if (!fail) - { - magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); - stats.setMagicka(magicka); - } - - // If this is a power, check if it was already used in last 24h - if (!fail && spell->mData.mType & ESM::Spell::ST_Power) - { - if (stats.canUsePower(selectedSpell)) - stats.usePower(selectedSpell); - else - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sPowerAlreadyUsed}"); - fail = true; - } - } - - // Check success - int successChance = MWMechanics::getSpellSuccessChance(selectedSpell, actor); - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (!fail && roll >= successChance) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); - fail = true; - } - - if (fail) - { - // Failure sound - for (std::vector::const_iterator iter (spell->mEffects.mList.begin()); - iter!=spell->mEffects.mList.end(); ++iter) - { - const ESM::MagicEffect *magicEffect = getStore().get().find ( - iter->mEffectID); - - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(actor, "Spell Failure " + schools[magicEffect->mData.mSchool], 1.0f, 1.0f); - break; - } - return; - } - - if (actor == getPlayer().getPlayer() && spell->mData.mType == ESM::Spell::ST_Spell) - actor.getClass().skillUsageSucceeded(actor, - MWMechanics::spellSchoolToSkill(MWMechanics::getSpellSchool(selectedSpell, actor)), 0); - - effects = spell->mEffects; + cast.cast(spell); } - InventoryStore& inv = actor.getClass().getInventoryStore(actor); - if (selectedSpell.empty() && inv.getSelectedEnchantItem() != inv.end()) + else if (inv.getSelectedEnchantItem() != inv.end()) { - MWWorld::Ptr item = *inv.getSelectedEnchantItem(); - selectedSpell = item.getClass().getEnchantment(item); - const ESM::Enchantment* enchantment = getStore().get().search (selectedSpell); - - if (enchantment->mData.mType == ESM::Enchantment::WhenUsed) - { - // Check if there's enough charge left - const float enchantCost = enchantment->mData.mCost; - MWMechanics::NpcStats &stats = MWWorld::Class::get(actor).getNpcStats(actor); - int eSkill = stats.getSkill(ESM::Skill::Enchant).getModified(); - const float castCost = enchantCost - (enchantCost / 100) * (eSkill - 10); - - if (item.getCellRef().mEnchantmentCharge == -1) - item.getCellRef().mEnchantmentCharge = enchantment->mData.mCharge; - - if (item.getCellRef().mEnchantmentCharge < castCost) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); - return; - } - - // Reduce charge - item.getCellRef().mEnchantmentCharge -= castCost; - } - if (enchantment->mData.mType == ESM::Enchantment::CastOnce) - { - if (!item.getContainerStore()->remove(item, 1, actor)) - { - // Item was used up - MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); - inv.setSelectedEnchantItem(inv.end()); - } - } - else - MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(item); // Set again to show the modified charge - - sourceName = item.getClass().getName(item); - - effects = enchantment->mEffects; + cast.cast(*inv.getSelectedEnchantItem()); } - - // Now apply the spell! - - // Apply Self portion - actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(selectedSpell, actor, actor, ESM::RT_Self, sourceName); - - // Apply Touch portion - // TODO: Distance is probably incorrect, and should it be hardcoded? - std::pair contact = getHitContact(actor, 100); - if (!contact.first.isEmpty()) - { - if (contact.first.getClass().isActor()) - { - if (!contact.first.getClass().getCreatureStats(contact.first).isDead()) - contact.first.getClass().getCreatureStats(contact.first).getActiveSpells().addSpell(selectedSpell, contact.first, actor, ESM::RT_Touch, sourceName); - } - else - { - // We hit a non-actor, e.g. a door. Only instant effects are relevant. - // inflictSpellOnNonActor(contact.first, selectedSpell, ESM::RT_Touch); - } - } - - launchProjectile(selectedSpell, effects, actor, sourceName); - } void World::launchProjectile (const std::string& id, const ESM::EffectList& effects,