From cbe96a217020c0424a76a6256b40bc25f34b9a7e Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 9 Nov 2013 07:51:46 +0100 Subject: [PATCH 1/3] Refactor ActiveSpells to track range type. Added basic self range magic. --- apps/openmw/mwbase/world.hpp | 2 + apps/openmw/mwgui/spellicons.cpp | 27 ++++++--- apps/openmw/mwgui/tooltips.cpp | 2 +- apps/openmw/mwmechanics/activespells.cpp | 74 ++++++++++++++++++------ apps/openmw/mwmechanics/activespells.hpp | 26 ++++++++- apps/openmw/mwmechanics/alchemy.cpp | 2 +- apps/openmw/mwmechanics/character.cpp | 5 ++ apps/openmw/mwrender/animation.cpp | 3 + apps/openmw/mwworld/worldimp.cpp | 73 +++++++++++++++++++++++ apps/openmw/mwworld/worldimp.hpp | 2 + 10 files changed, 187 insertions(+), 29 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index c39e87826..8ae563e12 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -406,6 +406,8 @@ namespace MWBase virtual bool getGodModeState() = 0; virtual bool toggleGodMode() = 0; + + virtual void castSpell (const MWWorld::Ptr& actor) = 0; }; } diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 0c303485a..5d002792a 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -97,6 +97,8 @@ namespace MWGui } // add lasting effect spells/potions etc + + // TODO: Move this to ActiveSpells const MWMechanics::ActiveSpells::TContainer& activeSpells = stats.getActiveSpells().getActiveSpells(); for (MWMechanics::ActiveSpells::TContainer::const_iterator it = activeSpells.begin(); it != activeSpells.end(); ++it) @@ -105,31 +107,36 @@ namespace MWGui float timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); + int i=0; for (std::vector::const_iterator effectIt = list.mList.begin(); - effectIt != list.mList.end(); ++effectIt) + effectIt != list.mList.end(); ++effectIt, ++i) { + if (effectIt->mRange != it->second.mRange) + continue; + + float randomFactor = it->second.mRandom[i]; + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectIt->mEffectID); MagicEffectInfo effectInfo; - effectInfo.mSource = getSpellDisplayName (it->first); + effectInfo.mSource = it->second.mName; effectInfo.mKey = MWMechanics::EffectKey (effectIt->mEffectID); if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) effectInfo.mKey.mArg = effectIt->mSkill; else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) effectInfo.mKey.mArg = effectIt->mAttribute; - effectInfo.mMagnitude = effectIt->mMagnMin + (effectIt->mMagnMax-effectIt->mMagnMin) * it->second.second; + effectInfo.mMagnitude = effectIt->mMagnMin + (effectIt->mMagnMax-effectIt->mMagnMin) * randomFactor; effectInfo.mRemainingTime = effectIt->mDuration + - (it->second.first - MWBase::Environment::get().getWorld()->getTimeStamp())*3600/timeScale; + (it->second.mTimeStamp - MWBase::Environment::get().getWorld()->getTimeStamp())*3600/timeScale; // ingredients need special casing for their magnitude / duration - /// \todo duplicated from ActiveSpells, helper function? if (MWBase::Environment::get().getWorld()->getStore().get().search (it->first)) { - effectInfo.mRemainingTime = effectIt->mDuration * it->second.second + - (it->second.first - MWBase::Environment::get().getWorld()->getTimeStamp())*3600/timeScale; + effectInfo.mRemainingTime = effectIt->mDuration * randomFactor + + (it->second.mTimeStamp - MWBase::Environment::get().getWorld()->getTimeStamp())*3600/timeScale; - effectInfo.mMagnitude = static_cast (0.05*it->second.second / (0.1 * magicEffect->mData.mBaseCost)); + effectInfo.mMagnitude = static_cast (0.05*randomFactor / (0.1 * magicEffect->mData.mBaseCost)); } effects[effectIt->mEffectID].push_back (effectInfo); @@ -288,6 +295,10 @@ namespace MWGui ESM::EffectList SpellIcons::getSpellEffectList (const std::string& id) { + if (const ESM::Enchantment* enchantment = + MWBase::Environment::get().getWorld()->getStore().get().search (id)) + return enchantment->mEffects; + if (const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().search (id)) return spell->mEffects; diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 3a609aa91..85c71575b 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -467,7 +467,7 @@ namespace MWGui } Widgets::MWDynamicStatPtr chargeWidget = enchantArea->createWidget ("MW_ChargeBar", chargeCoord, MyGUI::Align::Default, "ToolTipEnchantCharge"); - chargeWidget->setValue(charge, charge); + chargeWidget->setValue(charge, maxCharge); totalSize.height += 24; } } diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 9aca6b7b7..c433ac5e7 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -15,6 +15,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" @@ -64,15 +65,19 @@ namespace MWMechanics { std::pair > effects = getEffectList (iter->first); - const MWWorld::TimeStamp& start = iter->second.first; - float magnitude = iter->second.second; + const MWWorld::TimeStamp& start = iter->second.mTimeStamp; - for (std::vector::const_iterator iter (effects.first.mList.begin()); - iter!=effects.first.mList.end(); ++iter) + int i = 0; + for (std::vector::const_iterator effectIter (effects.first.mList.begin()); + effectIter!=effects.first.mList.end(); ++effectIter, ++i) { - if (iter->mDuration) + float magnitude = iter->second.mRandom[i]; + if (effectIter->mRange != iter->second.mRange) + continue; + + if (effectIter->mDuration) { - int duration = iter->mDuration; + int duration = effectIter->mDuration; if (effects.second.first) duration *= magnitude; @@ -89,9 +94,9 @@ namespace MWMechanics { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( - iter->mEffectID); + effectIter->mEffectID); - if (iter->mDuration==0) + if (effectIter->mDuration==0) { param.mMagnitude = static_cast (magnitude / (0.1 * magicEffect->mData.mBaseCost)); @@ -104,9 +109,9 @@ namespace MWMechanics } else param.mMagnitude = static_cast ( - (iter->mMagnMax-iter->mMagnMin)*magnitude + iter->mMagnMin); + (effectIter->mMagnMax-effectIter->mMagnMin)*magnitude + effectIter->mMagnMin); - mEffects.add (*iter, param); + mEffects.add (*effectIter, param); } } } @@ -115,6 +120,10 @@ namespace MWMechanics 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)); @@ -156,7 +165,7 @@ namespace MWMechanics : mSpellsChanged (false), mLastUpdate (MWBase::Environment::get().getWorld()->getTimeStamp()) {} - bool ActiveSpells::addSpell (const std::string& id, const MWWorld::Ptr& actor) + bool ActiveSpells::addSpell (const std::string& id, const MWWorld::Ptr& actor, ESM::RangeType range, const std::string& name) { std::pair > effects = getEffectList (id); bool stacks = effects.second.second; @@ -196,11 +205,41 @@ namespace MWMechanics random *= 0.25 * x; } + ActiveSpellParams params; + for (unsigned int i=0; i (std::rand()) / RAND_MAX); + params.mRange = range; + params.mTimeStamp = MWBase::Environment::get().getWorld()->getTimeStamp(); + params.mName = name; + if (iter==mSpells.end() || stacks) - mSpells.insert (std::make_pair (id, - std::make_pair (MWBase::Environment::get().getWorld()->getTimeStamp(), random))); + mSpells.insert (std::make_pair (id, params)); else - iter->second = std::make_pair (MWBase::Environment::get().getWorld()->getTimeStamp(), random); + iter->second = params; + + // Play sounds + for (std::vector::const_iterator iter (effects.first.mList.begin()); + iter!=effects.first.mList.end(); ++iter) + { + if (iter->mRange != range) + continue; + + const ESM::MagicEffect *magicEffect = + MWBase::Environment::get().getWorld()->getStore().get().find ( + iter->mEffectID); + + 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); + + break; + } mSpellsChanged = true; @@ -249,13 +288,14 @@ namespace MWMechanics duration = iter->mDuration; } - if (effects.second.first) - duration *= iterator->second.second; + // 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.first; + double usedUp = MWBase::Environment::get().getWorld()->getTimeStamp()-iterator->second.mTimeStamp; if (usedUp>=scaledDuration) return 0; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 8c859b2cb..e3f882b9a 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -9,6 +9,8 @@ #include "magiceffects.hpp" +#include + namespace ESM { struct Spell; @@ -22,6 +24,22 @@ namespace MWWorld 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; + + // 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 @@ -30,7 +48,7 @@ namespace MWMechanics { public: - typedef std::multimap > TContainer; + typedef std::multimap TContainer; typedef TContainer::const_iterator TIterator; private: @@ -51,9 +69,13 @@ namespace MWMechanics ActiveSpells(); - bool addSpell (const std::string& id, const MWWorld::Ptr& actor); + bool addSpell (const std::string& id, const MWWorld::Ptr& actor, ESM::RangeType range = ESM::RT_Self, const std::string& name = ""); ///< 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 + /// @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 /// /// \return Has the spell been added? diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 1d992be41..adfd6d5fc 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -273,7 +273,7 @@ void MWMechanics::Alchemy::addPotion (const std::string& name) newRecord.mName = name; - int index = static_cast (std::rand()/static_cast (RAND_MAX)*6); + int index = static_cast (std::rand()/static_cast (RAND_MAX+1)*6); assert (index>=0 && index<6); static const char *name[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index c4260d907..8c88b3ad0 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -531,6 +531,11 @@ bool CharacterController::updateNpcState(bool onground, bool inwater, bool isrun else sndMgr->playSound3D(mPtr, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } + if (inv.getSelectedEnchantItem() != inv.end()) + { + // Enchanted items cast immediately (no animation) + MWBase::Environment::get().getWorld()->castSpell(mPtr); + } } else if(mWeaponType == WeapType_PickProbe) { diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 545060fe3..53d697fdd 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -653,6 +653,9 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co MWWorld::Class::get(mPtr).hit(mPtr, MWMechanics::CreatureStats::AT_Thrust); else if(evt.compare(off, len, "hit") == 0) MWWorld::Class::get(mPtr).hit(mPtr); + + else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release") + MWBase::Environment::get().getWorld()->castSpell(mPtr); } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 9635feaf3..2c8236f3b 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -19,6 +19,8 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellsuccess.hpp" + #include "../mwrender/sky.hpp" #include "../mwrender/animation.hpp" @@ -1983,4 +1985,75 @@ namespace MWWorld return mGodMode; } + void World::castSpell(const Ptr &actor) + { + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + stats.setAttackingOrSpell(false); + + std::string selectedSpell = stats.getSpells().getSelectedSpell(); + if (!selectedSpell.empty()) + { + const ESM::Spell* spell = getStore().get().search(selectedSpell); + + // Check mana + MWMechanics::DynamicStat magicka = stats.getMagicka(); + if (magicka.getCurrent() < spell->mData.mCost) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientSP}"); + return; + } + + // Reduce mana + magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); + stats.setMagicka(magicka); + + // Check success + int successChance = MWMechanics::getSpellSuccessChance(selectedSpell, actor); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (roll >= successChance) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); + // TODO sound: "Spell Failure " + return; + } + + + actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(selectedSpell, actor, ESM::RT_Self); + // TODO: RT_Range, RT_Touch + return; + } + + InventoryStore& inv = actor.getClass().getInventoryStore(actor); + if (inv.getSelectedEnchantItem() != inv.end()) + { + MWWorld::Ptr item = *inv.getSelectedEnchantItem(); + std::string id = item.getClass().getEnchantment(item); + const ESM::Enchantment* enchantment = getStore().get().search (id); + + // 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; + + std::string itemName = item.getClass().getName(item); + actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(id, actor, ESM::RT_Self, itemName); + + + // TODO: RT_Range, RT_Touch + } + } + } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index e275756e8..6d05df6ff 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -453,6 +453,8 @@ namespace MWWorld virtual bool getGodModeState(); virtual bool toggleGodMode(); + + virtual void castSpell (const MWWorld::Ptr& actor); }; } From 1051611ffaba6fcf7dbb09830ecd2f7786fa50af Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 9 Nov 2013 07:59:17 +0100 Subject: [PATCH 2/3] Added spell failure sound --- apps/openmw/mwworld/worldimp.cpp | 33 +++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 2c8236f3b..5b7eb7bb1 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1996,24 +1996,47 @@ namespace MWWorld const ESM::Spell* spell = getStore().get().search(selectedSpell); // Check mana + bool fail = false; MWMechanics::DynamicStat magicka = stats.getMagicka(); if (magicka.getCurrent() < spell->mData.mCost) { MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientSP}"); - return; + fail = true; } // Reduce mana - magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); - stats.setMagicka(magicka); + if (!fail) + { + magicka.setCurrent(magicka.getCurrent() - spell->mData.mCost); + stats.setMagicka(magicka); + } // Check success int successChance = MWMechanics::getSpellSuccessChance(selectedSpell, actor); int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (roll >= successChance) + if (!fail && roll >= successChance) { MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); - // TODO sound: "Spell Failure " + 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; } From 976344f0a352c6904c04a6f13996aa140035b520 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 9 Nov 2013 08:07:40 +0100 Subject: [PATCH 3/3] Handle CastOnce enchantments --- apps/openmw/mwworld/worldimp.cpp | 45 ++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 5b7eb7bb1..a91f1e00c 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2053,27 +2053,44 @@ namespace MWWorld std::string id = item.getClass().getEnchantment(item); const ESM::Enchantment* enchantment = getStore().get().search (id); - // 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) + if (enchantment->mData.mType == ESM::Enchantment::WhenUsed) { - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); - return; - } + // 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); - // Reduce charge - item.getCellRef().mEnchantmentCharge -= castCost; + 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) + { + item.getRefData().setCount(item.getRefData().getCount()-1); + } std::string itemName = item.getClass().getName(item); actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(id, actor, ESM::RT_Self, itemName); + if (!item.getRefData().getCount()) + { + // 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 + // TODO: RT_Range, RT_Touch }