diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 3395b5fc88..bee3641f8e 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -85,7 +85,7 @@ add_openmw_dir (mwmechanics drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate coordinateconverter trading weaponpriority spellpriority weapontype + character actors objects aistate coordinateconverter trading weaponpriority spellpriority weapontype spellutil tickableeffects ) add_openmw_dir (mwstate diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 39278f0fab..8449e6a5b6 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -19,7 +19,7 @@ #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 23f24e321f..a567d114b0 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -15,10 +15,10 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spells.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/spellutil.hpp" #include "tooltips.hpp" #include "class.hpp" diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index cbe664ab1d..1dedfa10b1 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -7,7 +7,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/creaturestats.hpp" -#include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 41be4f3a8f..7776b376a3 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -18,7 +18,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" -#include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/spells.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/actorutil.hpp" diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index e3250e5fef..c0db57b1b1 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -17,7 +17,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/actorutil.hpp" #include "mapwindow.hpp" diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 726b2a31fd..ba3dc1725c 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -40,6 +40,7 @@ #include "summoning.hpp" #include "combat.hpp" #include "actorutil.hpp" +#include "tickableeffects.hpp" namespace { diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 9f698b630a..c26454aab5 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -14,7 +14,6 @@ #include "../mwworld/cellstore.hpp" #include "npcstats.hpp" -#include "spellcasting.hpp" #include "combat.hpp" #include "weaponpriority.hpp" #include "spellpriority.hpp" diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index f55bebfc9f..6d30909180 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -1,5 +1,4 @@ #include "autocalcspell.hpp" -#include "spellcasting.hpp" #include @@ -8,6 +7,7 @@ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "spellutil.hpp" namespace MWMechanics { diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index fdf25b7c68..c71516090c 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -13,7 +13,7 @@ #include "../mwbase/mechanicsmanager.hpp" #include "creaturestats.hpp" -#include "spellcasting.hpp" +#include "spellutil.hpp" #include "actorutil.hpp" #include "weapontype.hpp" diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 695abe1058..25b33c4867 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -21,7 +21,7 @@ #include "aicombat.hpp" #include "aipursue.hpp" -#include "spellcasting.hpp" +#include "spellutil.hpp" #include "autocalcspell.hpp" #include "npcstats.hpp" #include "actorutil.hpp" @@ -376,7 +376,7 @@ namespace MWMechanics { const std::string& spell = winMgr->getSelectedSpell(); if (!spell.empty()) - winMgr->setSelectedSpell(spell, int(MWMechanics::getSpellSuccessChance(spell, mWatched))); + winMgr->setSelectedSpell(spell, int(getSpellSuccessChance(spell, mWatched))); else winMgr->unsetSelectedSpell(); } diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index d5ddd9a55a..bd61e77988 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -1,8 +1,5 @@ #include "spellcasting.hpp" -#include -#include - #include #include #include @@ -29,185 +26,11 @@ #include "weapontype.hpp" #include "summoning.hpp" #include "spellresistance.hpp" +#include "spellutil.hpp" +#include "tickableeffects.hpp" namespace MWMechanics { - ESM::Skill::SkillEnum spellSchoolToSkill(int school) - { - static const std::array schoolSkillArray - { - ESM::Skill::Alteration, ESM::Skill::Conjuration, ESM::Skill::Destruction, - ESM::Skill::Illusion, ESM::Skill::Mysticism, ESM::Skill::Restoration - }; - return schoolSkillArray.at(school); - } - - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect) - { - if (!magicEffect) - magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); - bool hasMagnitude = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude); - bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - int minMagn = hasMagnitude ? effect.mMagnMin : 1; - int maxMagn = hasMagnitude ? effect.mMagnMax : 1; - int duration = hasDuration ? effect.mDuration : 1; - static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore() - .get().find("fEffectCostMult")->mValue.getFloat(); - - float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); - x *= 0.1 * magicEffect->mData.mBaseCost; - x *= 1 + duration; - x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; - - return x * fEffectCostMult; - } - - float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool) - { - // Morrowind for some reason uses a formula slightly different from magicka cost calculation - float y = std::numeric_limits::max(); - float lowestSkill = 0; - - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) - { - float x = static_cast(effect.mDuration); - const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); - - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) - x = std::max(1.f, x); - - x *= 0.1f * magicEffect->mData.mBaseCost; - x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); - x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; - if (effect.mRange == ESM::RT_Target) - x *= 1.5f; - static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fEffectCostMult")->mValue.getFloat(); - x *= fEffectCostMult; - - float s = 2.0f * actor.getClass().getSkill(actor, spellSchoolToSkill(magicEffect->mData.mSchool)); - if (s - x < y) - { - y = s - x; - if (effectiveSchool) - *effectiveSchool = magicEffect->mData.mSchool; - lowestSkill = s; - } - } - - CreatureStats& stats = actor.getClass().getCreatureStats(actor); - - int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); - int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); - - float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck); - - return castChance; - } - - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) - { - bool godmode = actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - CreatureStats& stats = actor.getClass().getCreatureStats(actor); - - float castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude(); - - float castChance = calcSpellBaseSuccessChance(spell, actor, effectiveSchool) + castBonus; - castChance *= stats.getFatigueTerm(); - - if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude()&& !godmode) - return 0; - - if (spell->mData.mType == ESM::Spell::ST_Power) - return stats.getSpells().canUsePower(spell) ? 100 : 0; - - if (spell->mData.mType != ESM::Spell::ST_Spell) - return 100; - - if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost && !godmode) - return 0; - - if (spell->mData.mFlags & ESM::Spell::F_Always) - return 100; - - if (godmode) - return 100; - - return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); - } - - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) - { - if (const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId)) - return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka); - return 0.f; - } - - int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor) - { - int school = 0; - getSpellSuccessChance(spellId, actor, &school); - return school; - } - - int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) - { - int school = 0; - getSpellSuccessChance(spell, actor, &school); - return school; - } - - bool spellIncreasesSkill(const ESM::Spell *spell) - { - return spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always); - } - - bool spellIncreasesSkill(const std::string &spellId) - { - const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); - return spell && spellIncreasesSkill(spell); - } - - /// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure. - bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer) - { - switch (effectId) - { - case ESM::MagicEffect::Levitate: - if (!MWBase::Environment::get().getWorld()->isLevitationEnabled()) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); - return false; - } - break; - case ESM::MagicEffect::Soultrap: - if (!target.getClass().isNpc() // no messagebox for NPCs - && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); - return true; // must still apply to get visual effect and have target regard it as attack - } - break; - case ESM::MagicEffect::WaterWalking: - if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target)) - return false; - - MWBase::World *world = MWBase::Environment::get().getWorld(); - - if (!world->isWaterWalkingCastableOnTarget(target)) - { - if (castByPlayer && caster == target) - MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}"); - return false; - } - break; - } - return true; - } - class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor { public: @@ -1016,232 +839,4 @@ namespace MWMechanics { return !mManualSpell && MWMechanics::spellIncreasesSkill(mId); } - - int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) - { - /* - * Each point of enchant skill above/under 10 subtracts/adds - * one percent of enchantment cost while minimum is 1. - */ - int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant); - const float result = castCost - (castCost / 100) * (eSkill - 10); - - return static_cast((result < 1) ? 1 : result); - } - - bool disintegrateSlot (MWWorld::Ptr ptr, int slot, float disintegrate) - { - if (ptr.getClass().hasInventoryStore(ptr)) - { - MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator item = inv.getSlot(slot); - - if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) - { - if (!item->getClass().hasItemHealth(*item)) - return false; - int charge = item->getClass().getItemHealth(*item); - - if (charge == 0) - return false; - - // Store remainder of disintegrate amount (automatically subtracted if > 1) - item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); - - charge = item->getClass().getItemHealth(*item); - charge -= std::min(static_cast(disintegrate), charge); - item->getCellRef().setCharge(charge); - - if (charge == 0) - { - // Will unequip the broken item and try to find a replacement - if (ptr != getPlayer()) - inv.autoEquip(ptr); - else - inv.unequipItem(*item, ptr); - } - - return true; - } - } - return false; - } - - void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) - { - DynamicStat stat = creatureStats.getDynamic(index); - stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); - creatureStats.setDynamic(index, stat); - } - - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) - { - if (magnitude == 0.f) - return false; - - bool receivedMagicDamage = false; - - switch (effectKey.mId) - { - case ESM::MagicEffect::DamageAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.damage(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.restore(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); - break; - case ESM::MagicEffect::DamageHealth: - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); - break; - - case ESM::MagicEffect::DamageMagicka: - case ESM::MagicEffect::DamageFatigue: - { - int index = effectKey.mId-ESM::MagicEffect::DamageHealth; - static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); - adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); - break; - } - case ESM::MagicEffect::AbsorbHealth: - if (magnitude > 0.f) - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - - break; - - case ESM::MagicEffect::AbsorbMagicka: - case ESM::MagicEffect::AbsorbFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - break; - - case ESM::MagicEffect::DisintegrateArmor: - { - // According to UESP - int priorities[] = { - MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, - MWWorld::InventoryStore::Slot_RightGauntlet, - MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots - }; - - for (unsigned int i=0; iisExterior()) - break; - float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); - float damageScale = 1.f - timeDiff / 7.f; - // When cloudy, the sun damage effect is halved - static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicSunBlockedMult")->mValue.getFloat(); - - int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); - if (weather > 1) - damageScale *= fMagicSunBlockedMult; - - adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); - if (magnitude * damageScale > 0.f) - receivedMagicDamage = true; - - break; - } - - case ESM::MagicEffect::FireDamage: - case ESM::MagicEffect::ShockDamage: - case ESM::MagicEffect::FrostDamage: - case ESM::MagicEffect::Poison: - { - adjustDynamicStat(creatureStats, 0, -magnitude); - receivedMagicDamage = true; - break; - } - - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::RestoreSkill: - { - if (!actor.getClass().isNpc()) - break; - NpcStats &npcStats = actor.getClass().getNpcStats(actor); - SkillValue& skill = npcStats.getSkill(effectKey.mArg); - if (effectKey.mId == ESM::MagicEffect::RestoreSkill) - skill.restore(magnitude); - else - skill.damage(magnitude); - break; - } - - case ESM::MagicEffect::CurePoison: - actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); - break; - case ESM::MagicEffect::CureParalyzation: - actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); - break; - case ESM::MagicEffect::CureCommonDisease: - actor.getClass().getCreatureStats(actor).getSpells().purgeCommonDisease(); - break; - case ESM::MagicEffect::CureBlightDisease: - actor.getClass().getCreatureStats(actor).getSpells().purgeBlightDisease(); - break; - case ESM::MagicEffect::CureCorprusDisease: - actor.getClass().getCreatureStats(actor).getSpells().purgeCorprusDisease(); - break; - case ESM::MagicEffect::RemoveCurse: - actor.getClass().getCreatureStats(actor).getSpells().purgeCurses(); - break; - default: - return false; - } - - if (receivedMagicDamage && actor == getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return true; - } - - void ApplyLoopingParticlesVisitor::visit (MWMechanics::EffectKey key, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float /*magnitude*/, float /*remainingTime*/, float /*totalTime*/) - { - const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(key.mId); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) - return; - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - else - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); - } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 91cf372729..3fcec8f4a6 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -2,13 +2,10 @@ #define MWMECHANICS_SPELLCASTING_H #include -#include #include #include "../mwworld/ptr.hpp" -#include "magiceffects.hpp" - namespace ESM { struct Spell; @@ -23,38 +20,6 @@ namespace MWMechanics class MagicEffects; class CreatureStats; - ESM::Skill::SkillEnum spellSchoolToSkill(int school); - - float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr); - - /** - * @param spell spell to cast - * @param actor calculate spell success chance for this actor (depends on actor's skills) - * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here - * @param cap cap the result to 100%? - * @param checkMagicka check magicka? - * @note actor can be an NPC or a creature - * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. - */ - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); - - int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); - int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); - - /// Get whether or not the given spell contributes to skill progress. - bool spellIncreasesSkill(const ESM::Spell* spell); - bool spellIncreasesSkill(const std::string& spellId); - - bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer); - - int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); - float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool); - - /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed - /// @return Was the effect a tickable effect with a magnitude? - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const MWMechanics::EffectKey& effectKey, float magnitude); - class CastSpell { private: @@ -105,22 +70,6 @@ namespace MWMechanics /// @return was the target suitable for the effect? bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); }; - - class ApplyLoopingParticlesVisitor : public EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - - public: - ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) - : mActor(actor) - { - } - - virtual void visit (MWMechanics::EffectKey key, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1); - }; } #endif diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index a529781cbe..9428beafc0 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -15,11 +15,11 @@ #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" -#include "spellcasting.hpp" #include "spellresistance.hpp" #include "weapontype.hpp" #include "combat.hpp" #include "summoning.hpp" +#include "spellutil.hpp" namespace { diff --git a/apps/openmw/mwmechanics/spellresistance.cpp b/apps/openmw/mwmechanics/spellresistance.cpp index bbb4b56a5c..4868a7a250 100644 --- a/apps/openmw/mwmechanics/spellresistance.cpp +++ b/apps/openmw/mwmechanics/spellresistance.cpp @@ -9,7 +9,7 @@ #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" -#include "spellcasting.hpp" +#include "spellutil.hpp" namespace MWMechanics { diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp new file mode 100644 index 0000000000..cce07f9e3f --- /dev/null +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -0,0 +1,208 @@ +#include "spellutil.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +#include "actorutil.hpp" +#include "creaturestats.hpp" + +namespace MWMechanics +{ + ESM::Skill::SkillEnum spellSchoolToSkill(int school) + { + static const std::array schoolSkillArray + { + ESM::Skill::Alteration, ESM::Skill::Conjuration, ESM::Skill::Destruction, + ESM::Skill::Illusion, ESM::Skill::Mysticism, ESM::Skill::Restoration + }; + return schoolSkillArray.at(school); + } + + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect) + { + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + if (!magicEffect) + magicEffect = store.get().find(effect.mEffectID); + bool hasMagnitude = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude); + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); + int minMagn = hasMagnitude ? effect.mMagnMin : 1; + int maxMagn = hasMagnitude ? effect.mMagnMax : 1; + int duration = hasDuration ? effect.mDuration : 1; + static const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); + + float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); + x *= 0.1 * magicEffect->mData.mBaseCost; + x *= 1 + duration; + x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; + + return x * fEffectCostMult; + } + + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) + { + /* + * Each point of enchant skill above/under 10 subtracts/adds + * one percent of enchantment cost while minimum is 1. + */ + int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant); + const float result = castCost - (castCost / 100) * (eSkill - 10); + + return static_cast((result < 1) ? 1 : result); + } + + float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool) + { + // Morrowind for some reason uses a formula slightly different from magicka cost calculation + float y = std::numeric_limits::max(); + float lowestSkill = 0; + + for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + { + float x = static_cast(effect.mDuration); + const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) + x = std::max(1.f, x); + + x *= 0.1f * magicEffect->mData.mBaseCost; + x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); + x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; + if (effect.mRange == ESM::RT_Target) + x *= 1.5f; + static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get().find( + "fEffectCostMult")->mValue.getFloat(); + x *= fEffectCostMult; + + float s = 2.0f * actor.getClass().getSkill(actor, spellSchoolToSkill(magicEffect->mData.mSchool)); + if (s - x < y) + { + y = s - x; + if (effectiveSchool) + *effectiveSchool = magicEffect->mData.mSchool; + lowestSkill = s; + } + } + + CreatureStats& stats = actor.getClass().getCreatureStats(actor); + + int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); + int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); + + float castChance = (lowestSkill - spell->mData.mCost + 0.2f * actorWillpower + 0.1f * actorLuck); + + return castChance; + } + + float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) + { + bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + + CreatureStats& stats = actor.getClass().getCreatureStats(actor); + + if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() && !godmode) + return 0; + + if (spell->mData.mType == ESM::Spell::ST_Power) + return stats.getSpells().canUsePower(spell) ? 100 : 0; + + if (godmode) + return 100; + + if (spell->mData.mType != ESM::Spell::ST_Spell) + return 100; + + if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost) + return 0; + + if (spell->mData.mFlags & ESM::Spell::F_Always) + return 100; + + float castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude(); + float castChance = calcSpellBaseSuccessChance(spell, actor, effectiveSchool) + castBonus; + castChance *= stats.getFatigueTerm(); + + return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); + } + + float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) + { + if (const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId)) + return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka); + return 0.f; + } + + int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor) + { + int school = 0; + getSpellSuccessChance(spellId, actor, &school); + return school; + } + + int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) + { + int school = 0; + getSpellSuccessChance(spell, actor, &school); + return school; + } + + bool spellIncreasesSkill(const ESM::Spell *spell) + { + return spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always); + } + + bool spellIncreasesSkill(const std::string &spellId) + { + const auto spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); + return spell && spellIncreasesSkill(spell); + } + + bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer) + { + switch (effectId) + { + case ESM::MagicEffect::Levitate: + { + if (!MWBase::Environment::get().getWorld()->isLevitationEnabled()) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); + return false; + } + break; + } + case ESM::MagicEffect::Soultrap: + { + if (!target.getClass().isNpc() // no messagebox for NPCs + && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); + return true; // must still apply to get visual effect and have target regard it as attack + } + break; + } + case ESM::MagicEffect::WaterWalking: + { + if (target.getClass().isPureWaterCreature(target) && MWBase::Environment::get().getWorld()->isSwimming(target)) + return false; + + MWBase::World *world = MWBase::Environment::get().getWorld(); + + if (!world->isWaterWalkingCastableOnTarget(target)) + { + if (castByPlayer && caster == target) + MWBase::Environment::get().getWindowManager()->messageBox ("#{sMagicInvalidEffect}"); + return false; + } + break; + } + } + return true; + } +} diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp new file mode 100644 index 0000000000..865a9126e7 --- /dev/null +++ b/apps/openmw/mwmechanics/spellutil.hpp @@ -0,0 +1,50 @@ +#ifndef MWMECHANICS_SPELLUTIL_H +#define MWMECHANICS_SPELLUTIL_H + +#include + +namespace ESM +{ + struct ENAMstruct; + struct MagicEffect; + struct Spell; +} + +namespace MWWorld +{ + class Ptr; +} + +namespace MWMechanics +{ + ESM::Skill::SkillEnum spellSchoolToSkill(int school); + + float calcEffectCost(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect = nullptr); + + int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); + + /** + * @param spell spell to cast + * @param actor calculate spell success chance for this actor (depends on actor's skills) + * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here + * @param cap cap the result to 100%? + * @param checkMagicka check magicka? + * @note actor can be an NPC or a creature + * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. + */ + float calcSpellBaseSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool); + float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); + float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = nullptr, bool cap=true, bool checkMagicka=true); + + int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); + int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); + + /// Get whether or not the given spell contributes to skill progress. + bool spellIncreasesSkill(const ESM::Spell* spell); + bool spellIncreasesSkill(const std::string& spellId); + + /// Check if the given effect can be applied to the target. If \a castByPlayer, emits a message box on failure. + bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, const MWWorld::Ptr& caster, bool castByPlayer); +} + +#endif diff --git a/apps/openmw/mwmechanics/tickableeffects.cpp b/apps/openmw/mwmechanics/tickableeffects.cpp new file mode 100644 index 0000000000..2e48375c9e --- /dev/null +++ b/apps/openmw/mwmechanics/tickableeffects.cpp @@ -0,0 +1,218 @@ +#include "tickableeffects.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" + +#include "actorutil.hpp" +#include "npcstats.hpp" + +namespace MWMechanics +{ + void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) + { + DynamicStat stat = creatureStats.getDynamic(index); + stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); + creatureStats.setDynamic(index, stat); + } + + bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) + { + if (!ptr.getClass().hasInventoryStore(ptr)) + return false; + + MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStoreIterator item = inv.getSlot(slot); + + if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) + { + if (!item->getClass().hasItemHealth(*item)) + return false; + int charge = item->getClass().getItemHealth(*item); + if (charge == 0) + return false; + + // Store remainder of disintegrate amount (automatically subtracted if > 1) + item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); + + charge = item->getClass().getItemHealth(*item); + charge -= std::min(static_cast(disintegrate), charge); + item->getCellRef().setCharge(charge); + + if (charge == 0) + { + // Will unequip the broken item and try to find a replacement + if (ptr != getPlayer()) + inv.autoEquip(ptr); + else + inv.unequipItem(*item, ptr); + } + + return true; + } + + return false; + } + + bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) + { + if (magnitude == 0.f) + return false; + + bool receivedMagicDamage = false; + + switch (effectKey.mId) + { + case ESM::MagicEffect::DamageAttribute: + { + AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); + attr.damage(magnitude); + creatureStats.setAttribute(effectKey.mArg, attr); + break; + } + case ESM::MagicEffect::RestoreAttribute: + { + AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); + attr.restore(magnitude); + creatureStats.setAttribute(effectKey.mArg, attr); + break; + } + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); + break; + case ESM::MagicEffect::DamageHealth: + receivedMagicDamage = true; + adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); + break; + + case ESM::MagicEffect::DamageMagicka: + case ESM::MagicEffect::DamageFatigue: + { + int index = effectKey.mId-ESM::MagicEffect::DamageHealth; + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); + break; + } + case ESM::MagicEffect::AbsorbHealth: + if (magnitude > 0.f) + receivedMagicDamage = true; + adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); + + break; + + case ESM::MagicEffect::AbsorbMagicka: + case ESM::MagicEffect::AbsorbFatigue: + adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); + break; + + case ESM::MagicEffect::DisintegrateArmor: + { + // According to UESP + int priorities[] = { + MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, + MWWorld::InventoryStore::Slot_RightGauntlet, + MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots + }; + + for (unsigned int i=0; iisExterior()) + break; + float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); + float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float damageScale = 1.f - timeDiff / 7.f; + // When cloudy, the sun damage effect is halved + static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( + "fMagicSunBlockedMult")->mValue.getFloat(); + + int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); + if (weather > 1) + damageScale *= fMagicSunBlockedMult; + + adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); + if (magnitude * damageScale > 0.f) + receivedMagicDamage = true; + + break; + } + + case ESM::MagicEffect::FireDamage: + case ESM::MagicEffect::ShockDamage: + case ESM::MagicEffect::FrostDamage: + case ESM::MagicEffect::Poison: + { + adjustDynamicStat(creatureStats, 0, -magnitude); + receivedMagicDamage = true; + break; + } + + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::RestoreSkill: + { + if (!actor.getClass().isNpc()) + break; + NpcStats &npcStats = actor.getClass().getNpcStats(actor); + SkillValue& skill = npcStats.getSkill(effectKey.mArg); + if (effectKey.mId == ESM::MagicEffect::RestoreSkill) + skill.restore(magnitude); + else + skill.damage(magnitude); + break; + } + + case ESM::MagicEffect::CurePoison: + actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); + break; + case ESM::MagicEffect::CureParalyzation: + actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); + break; + case ESM::MagicEffect::CureCommonDisease: + actor.getClass().getCreatureStats(actor).getSpells().purgeCommonDisease(); + break; + case ESM::MagicEffect::CureBlightDisease: + actor.getClass().getCreatureStats(actor).getSpells().purgeBlightDisease(); + break; + case ESM::MagicEffect::CureCorprusDisease: + actor.getClass().getCreatureStats(actor).getSpells().purgeCorprusDisease(); + break; + case ESM::MagicEffect::RemoveCurse: + actor.getClass().getCreatureStats(actor).getSpells().purgeCurses(); + break; + default: + return false; + } + + if (receivedMagicDamage && actor == getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + return true; + } +} diff --git a/apps/openmw/mwmechanics/tickableeffects.hpp b/apps/openmw/mwmechanics/tickableeffects.hpp new file mode 100644 index 0000000000..c4abed6a3a --- /dev/null +++ b/apps/openmw/mwmechanics/tickableeffects.hpp @@ -0,0 +1,19 @@ +#ifndef MWMECHANICS_TICKABLEEFFECTS_H +#define MWMECHANICS_TICKABLEEFFECTS_H + +namespace MWWorld +{ + class Ptr; +} + +namespace MWMechanics +{ + class CreatureStats; + struct EffectKey; + + /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed + /// @return Was the effect a tickable effect with a magnitude? + bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); +} + +#endif diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index 2e65012259..13ce309277 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -13,7 +13,7 @@ #include "combat.hpp" #include "aicombataction.hpp" #include "spellpriority.hpp" -#include "spellcasting.hpp" +#include "spellutil.hpp" #include "weapontype.hpp" namespace MWMechanics diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 1de97d068d..d4358532c4 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -13,8 +13,8 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 8c5f526551..11444c8ebf 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -20,7 +20,7 @@ #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" -#include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/spellutil.hpp" #include "class.hpp" #include "ptr.hpp" diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 37123099af..a623a5d52f 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3161,12 +3161,42 @@ namespace MWWorld mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); } + class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor + { + private: + MWWorld::Ptr mActor; + + public: + ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) + : mActor(actor) + { + } + + virtual void visit (MWMechanics::EffectKey key, + const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, + float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) + { + const ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const auto magicEffect = store.get().find(key.mId); + if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) + return; + const ESM::Static* castStatic; + if (!magicEffect->mHit.empty()) + castStatic = store.get().find (magicEffect->mHit); + else + castStatic = store.get().find ("VFX_DefaultHit"); + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); + if (anim && !castStatic->mModel.empty()) + anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); + } + }; + void World::applyLoopingParticles(const MWWorld::Ptr& ptr) { const MWWorld::Class &cls = ptr.getClass(); if (cls.isActor()) { - MWMechanics::ApplyLoopingParticlesVisitor visitor(ptr); + ApplyLoopingParticlesVisitor visitor(ptr); cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); if (cls.hasInventoryStore(ptr))