From b1a29eb27eafdc2db1bf7ffb416ac42aea25357f Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 16 Nov 2013 02:34:43 +0100 Subject: [PATCH] Implement Resist & Weakness effects --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwclass/npc.cpp | 6 +- apps/openmw/mwgui/quickkeysmenu.cpp | 2 +- apps/openmw/mwgui/spellwindow.cpp | 2 +- apps/openmw/mwmechanics/activespells.cpp | 104 +++++++++++------- apps/openmw/mwmechanics/activespells.hpp | 7 +- .../{spellsuccess.hpp => spellcasting.hpp} | 66 +++++++++++ apps/openmw/mwworld/actiontrap.cpp | 2 +- apps/openmw/mwworld/inventorystore.cpp | 36 +++--- apps/openmw/mwworld/worldimp.cpp | 6 +- 10 files changed, 159 insertions(+), 74 deletions(-) rename apps/openmw/mwmechanics/{spellsuccess.hpp => spellcasting.hpp} (64%) diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 04bd89f95..cc4a9d6de 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -69,7 +69,7 @@ add_openmw_dir (mwclass add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence alchemy aiwander aitravel aifollow - aiescort aiactivate repair enchanting pathfinding security spellsuccess + aiescort aiactivate repair enchanting pathfinding security spellsuccess spellcasting ) add_openmw_dir (mwbase diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 35a6d17a3..fd24dd93f 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -468,9 +468,9 @@ namespace MWClass { weapon.getCellRef().mEnchantmentCharge -= castCost; // Touch - othercls.getCreatureStats(victim).getActiveSpells().addSpell(enchantmentName, victim, ESM::RT_Touch, weapon.getClass().getName(weapon)); + othercls.getCreatureStats(victim).getActiveSpells().addSpell(enchantmentName, victim, ptr, ESM::RT_Touch, weapon.getClass().getName(weapon)); // Self - getCreatureStats(ptr).getActiveSpells().addSpell(enchantmentName, ptr, ESM::RT_Self, weapon.getClass().getName(weapon)); + 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)); } @@ -936,7 +936,7 @@ namespace MWClass /// \todo consider instant effects - return stats.getActiveSpells().addSpell (id, actor); + return stats.getActiveSpells().addSpell (id, actor, actor); } void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 5f749d3d3..503bf7c11 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -5,7 +5,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwmechanics/spellsuccess.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "../mwgui/inventorywindow.hpp" #include "../mwgui/bookwindow.hpp" #include "../mwgui/scrollwindow.hpp" diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 61ac2c7b2..03bb10631 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -9,7 +9,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/actionequip.hpp" -#include "../mwmechanics/spellsuccess.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "spellicons.hpp" #include "inventorywindow.hpp" diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index d96a5f351..3a9321f39 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -16,11 +16,14 @@ #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" @@ -74,7 +77,7 @@ namespace MWMechanics for (std::vector::const_iterator effectIter (effects.first.mList.begin()); effectIter!=effects.first.mList.end(); ++effectIter, ++i) { - float magnitude = iter->second.mRandom[i]; + float random = iter->second.mRandom[i]; if (effectIter->mRange != iter->second.mRange) continue; @@ -83,7 +86,7 @@ namespace MWMechanics int duration = effectIter->mDuration; if (effects.second.first) - duration *= magnitude; + duration *= random; MWWorld::TimeStamp end = start; end += static_cast (duration)* @@ -102,19 +105,21 @@ namespace MWMechanics if (effectIter->mDuration==0) { param.mMagnitude = - static_cast (magnitude / (0.1 * magicEffect->mData.mBaseCost)); + static_cast (random / (0.1 * magicEffect->mData.mBaseCost)); } else { param.mMagnitude = - static_cast (0.05*magnitude / (0.1 * magicEffect->mData.mBaseCost)); + static_cast (0.05*random / (0.1 * magicEffect->mData.mBaseCost)); } } else param.mMagnitude = static_cast ( - (effectIter->mMagnMax-effectIter->mMagnMin)*magnitude + effectIter->mMagnMin); - - mEffects.add (*effectIter, param); + (effectIter->mMagnMax-effectIter->mMagnMin)*random + effectIter->mMagnMin); + param.mMagnitude *= iter->second.mMultiplier[i]; + + if (param.mMagnitude) + mEffects.add (*effectIter, param); } } } @@ -185,7 +190,7 @@ namespace MWMechanics : mSpellsChanged (false), mLastUpdate (MWBase::Environment::get().getWorld()->getTimeStamp()) {} - bool ActiveSpells::addSpell (const std::string& id, const MWWorld::Ptr& actor, ESM::RangeType range, const std::string& name, int effectIndex) + 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); @@ -197,6 +202,8 @@ namespace MWMechanics 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; @@ -204,41 +211,37 @@ namespace MWMechanics } } + // If none of the effects need to apply, no need to add the spell if (!found) return false; TContainer::iterator iter = mSpells.find (id); - float random = static_cast (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; - } - ActiveSpellParams params; for (unsigned int i=0; i (std::rand()) / RAND_MAX); + { + float random = static_cast (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; - - if (iter==mSpells.end() || stacks) - mSpells.insert (std::make_pair (id, params)); - else - iter->second = params; - - + params.mMultiplier.resize(effects.first.mList.size(), 1); /* for (int i=0; i::const_iterator iter (effects.first.mList.begin()); - iter!=effects.first.mList.end(); ++iter) + int i = 0; + for (std::vector::const_iterator effectIt (effects.first.mList.begin()); + effectIt!=effects.first.mList.end(); ++effectIt, ++i) { - if (iter->mRange != range) + if (effectIt->mRange != range) + continue; + + // 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. const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( - iter->mEffectID); + effectIt->mEffectID); // Only the sound of the first effect plays if (first) @@ -296,6 +315,11 @@ namespace MWMechanics first = false; } + if (iter==mSpells.end() || stacks) + mSpells.insert (std::make_pair (id, params)); + else + iter->second = params; + mSpellsChanged = true; return true; @@ -415,7 +439,9 @@ namespace MWMechanics magnitude = static_cast (0.05*it->second.mRandom[i] / (0.1 * magicEffect->mData.mBaseCost)); } - visitor.visit(*effectIt, name, magnitude, remainingTime); + magnitude *= it->second.mMultiplier[i]; + if (magnitude) + visitor.visit(*effectIt, name, magnitude, remainingTime); } } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 2cf30da94..b5c302afe 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -37,7 +37,7 @@ namespace MWMechanics // 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; + 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 @@ -85,11 +85,12 @@ namespace MWMechanics ActiveSpells(); - bool addSpell (const std::string& id, const MWWorld::Ptr& actor, ESM::RangeType range = ESM::RT_Self, const std::string& name = "", int effectIndex = -1); + 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 + /// @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 diff --git a/apps/openmw/mwmechanics/spellsuccess.hpp b/apps/openmw/mwmechanics/spellcasting.hpp similarity index 64% rename from apps/openmw/mwmechanics/spellsuccess.hpp rename to apps/openmw/mwmechanics/spellcasting.hpp index fc6af3c55..3bbb4d0c5 100644 --- a/apps/openmw/mwmechanics/spellsuccess.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -114,6 +114,72 @@ namespace MWMechanics return school; } + /// @return >=100 for fully resisted. can also return negative value for damage amplification. + inline float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = NULL) + { + const ESM::MagicEffect *magicEffect = + MWBase::Environment::get().getWorld()->getStore().get().find ( + effectId); + + const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + + float resisted = 0; + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) + { + + short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId); + short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId); + + float resistance = 0; + if (resistanceEffect != -1) + resistance += stats.getMagicEffects().get(resistanceEffect).mMagnitude; + if (weaknessEffect != -1) + resistance -= stats.getMagicEffects().get(weaknessEffect).mMagnitude; + + + float willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); + float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); + float x = (willpower + 0.1 * luck) * stats.getFatigueTerm(); + + // This makes spells that are easy to cast harder to resist and vice versa + if (spell != NULL) + { + float castChance = getSpellSuccessChance(spell, caster); + if (castChance > 0) + x *= 50 / castChance; + } + + float roll = static_cast(std::rand()) / RAND_MAX * 100; + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) + roll -= resistance; + + if (x <= roll) + x = 0; + else + { + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) + x = 100; + else + x = roll / std::min(x, 100.f); + } + + x = std::min(x + resistance, 100.f); + + resisted = x; + } + + return resisted; + } + + inline float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = NULL) + { + float resistance = getEffectResistance(effectId, actor, caster, spell); + if (resistance >= 0) + return 1 - resistance / 100.f; + else + return -(resistance-100) / 100.f; + } + } #endif diff --git a/apps/openmw/mwworld/actiontrap.cpp b/apps/openmw/mwworld/actiontrap.cpp index 13b2fd269..80da29072 100644 --- a/apps/openmw/mwworld/actiontrap.cpp +++ b/apps/openmw/mwworld/actiontrap.cpp @@ -16,7 +16,7 @@ namespace MWWorld // 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, ESM::RT_Touch); + actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(mSpellId, actor, actor, ESM::RT_Touch); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index bec059389..9e1869125 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -11,6 +11,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellcasting.hpp" + #include "esmstore.hpp" #include "class.hpp" @@ -292,47 +294,37 @@ void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) continue; - // Roll some dice, one for each effect std::vector params; - params.resize(enchantment.mEffects.mList.size()); - for (unsigned int i=0; i (std::rand()) / RAND_MAX; bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().mRefID) != mPermanentMagicEffectMagnitudes.end()); if (!existed) { + // Roll some dice, one for each effect + params.resize(enchantment.mEffects.mList.size()); + for (unsigned int i=0; i (std::rand()) / RAND_MAX; + // Try resisting each effect int i=0; for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); effectIt!=enchantment.mEffects.mList.end(); ++effectIt) { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effectIt->mEffectID); - - //const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - - float resisted = 0; - if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) - { - - } - params[i].mMultiplier = (100.f - resisted) / 100.f; - + params[i].mMultiplier = MWMechanics::getEffectMultiplier(effectIt->mEffectID, actor, actor); ++i; } - // Note that using the RefID as a key here is not entirely correct. // Consider equipping the same item twice (e.g. a ring) // However, permanent enchantments with a random magnitude are kind of an exploit anyway, // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. mPermanentMagicEffectMagnitudes[(**iter).getCellRef().mRefID] = params; } + else + params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().mRefID]; int i=0; for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); - effectIt!=enchantment.mEffects.mList.end(); ++effectIt) + effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( @@ -355,7 +347,6 @@ void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) magnitude *= params[i].mMultiplier; if (magnitude) mMagicEffects.add (*effectIt, magnitude); - ++i; } } } @@ -554,8 +545,9 @@ void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisito for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); effectIt!=enchantment.mEffects.mList.end(); ++effectIt) { - float random = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().mRefID][i].mRandom; - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; + 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); ++i; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index eca2ebb79..e6c4c1ef0 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -26,7 +26,7 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/spellsuccess.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "../mwrender/sky.hpp" @@ -2158,7 +2158,7 @@ namespace MWWorld // Now apply the spell! // Apply Self portion - actor.getClass().getCreatureStats(actor).getActiveSpells().addSpell(selectedSpell, actor, ESM::RT_Self, sourceName); + 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? @@ -2166,7 +2166,7 @@ namespace MWWorld if (!contact.first.isEmpty()) { if (contact.first.getClass().isActor()) - contact.first.getClass().getCreatureStats(contact.first).getActiveSpells().addSpell(selectedSpell, contact.first, ESM::RT_Touch, sourceName); + 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.