mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-26 18:35:20 +00:00
Separate functions that don't belong to CastSpell class
This commit is contained in:
parent
db13984db0
commit
8d22e075e6
@ -85,7 +85,7 @@ add_openmw_dir (mwmechanics
|
|||||||
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe
|
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
|
aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance
|
||||||
disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning
|
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
|
add_openmw_dir (mwstate
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
#include "../mwbase/windowmanager.hpp"
|
#include "../mwbase/windowmanager.hpp"
|
||||||
|
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
#include "../mwmechanics/creaturestats.hpp"
|
#include "../mwmechanics/creaturestats.hpp"
|
||||||
#include "../mwmechanics/actorutil.hpp"
|
#include "../mwmechanics/actorutil.hpp"
|
||||||
|
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
#include "../mwworld/class.hpp"
|
#include "../mwworld/class.hpp"
|
||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
|
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
|
||||||
#include "../mwmechanics/spells.hpp"
|
#include "../mwmechanics/spells.hpp"
|
||||||
#include "../mwmechanics/creaturestats.hpp"
|
#include "../mwmechanics/creaturestats.hpp"
|
||||||
#include "../mwmechanics/actorutil.hpp"
|
#include "../mwmechanics/actorutil.hpp"
|
||||||
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
|
|
||||||
#include "tooltips.hpp"
|
#include "tooltips.hpp"
|
||||||
#include "class.hpp"
|
#include "class.hpp"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
#include "../mwbase/windowmanager.hpp"
|
#include "../mwbase/windowmanager.hpp"
|
||||||
|
|
||||||
#include "../mwmechanics/creaturestats.hpp"
|
#include "../mwmechanics/creaturestats.hpp"
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
|
|
||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
#include "../mwworld/inventorystore.hpp"
|
#include "../mwworld/inventorystore.hpp"
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
#include "../mwworld/player.hpp"
|
#include "../mwworld/player.hpp"
|
||||||
|
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
#include "../mwmechanics/spells.hpp"
|
#include "../mwmechanics/spells.hpp"
|
||||||
#include "../mwmechanics/creaturestats.hpp"
|
#include "../mwmechanics/creaturestats.hpp"
|
||||||
#include "../mwmechanics/actorutil.hpp"
|
#include "../mwmechanics/actorutil.hpp"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
#include "../mwworld/class.hpp"
|
#include "../mwworld/class.hpp"
|
||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
#include "../mwmechanics/actorutil.hpp"
|
#include "../mwmechanics/actorutil.hpp"
|
||||||
|
|
||||||
#include "mapwindow.hpp"
|
#include "mapwindow.hpp"
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
#include "summoning.hpp"
|
#include "summoning.hpp"
|
||||||
#include "combat.hpp"
|
#include "combat.hpp"
|
||||||
#include "actorutil.hpp"
|
#include "actorutil.hpp"
|
||||||
|
#include "tickableeffects.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
#include "../mwworld/cellstore.hpp"
|
#include "../mwworld/cellstore.hpp"
|
||||||
|
|
||||||
#include "npcstats.hpp"
|
#include "npcstats.hpp"
|
||||||
#include "spellcasting.hpp"
|
|
||||||
#include "combat.hpp"
|
#include "combat.hpp"
|
||||||
#include "weaponpriority.hpp"
|
#include "weaponpriority.hpp"
|
||||||
#include "spellpriority.hpp"
|
#include "spellpriority.hpp"
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#include "autocalcspell.hpp"
|
#include "autocalcspell.hpp"
|
||||||
#include "spellcasting.hpp"
|
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
@ -8,6 +7,7 @@
|
|||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
#include "../mwbase/environment.hpp"
|
#include "../mwbase/environment.hpp"
|
||||||
|
|
||||||
|
#include "spellutil.hpp"
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
#include "../mwbase/mechanicsmanager.hpp"
|
#include "../mwbase/mechanicsmanager.hpp"
|
||||||
|
|
||||||
#include "creaturestats.hpp"
|
#include "creaturestats.hpp"
|
||||||
#include "spellcasting.hpp"
|
#include "spellutil.hpp"
|
||||||
#include "actorutil.hpp"
|
#include "actorutil.hpp"
|
||||||
#include "weapontype.hpp"
|
#include "weapontype.hpp"
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include "aicombat.hpp"
|
#include "aicombat.hpp"
|
||||||
#include "aipursue.hpp"
|
#include "aipursue.hpp"
|
||||||
#include "spellcasting.hpp"
|
#include "spellutil.hpp"
|
||||||
#include "autocalcspell.hpp"
|
#include "autocalcspell.hpp"
|
||||||
#include "npcstats.hpp"
|
#include "npcstats.hpp"
|
||||||
#include "actorutil.hpp"
|
#include "actorutil.hpp"
|
||||||
@ -376,7 +376,7 @@ namespace MWMechanics
|
|||||||
{
|
{
|
||||||
const std::string& spell = winMgr->getSelectedSpell();
|
const std::string& spell = winMgr->getSelectedSpell();
|
||||||
if (!spell.empty())
|
if (!spell.empty())
|
||||||
winMgr->setSelectedSpell(spell, int(MWMechanics::getSpellSuccessChance(spell, mWatched)));
|
winMgr->setSelectedSpell(spell, int(getSpellSuccessChance(spell, mWatched)));
|
||||||
else
|
else
|
||||||
winMgr->unsetSelectedSpell();
|
winMgr->unsetSelectedSpell();
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
#include "spellcasting.hpp"
|
#include "spellcasting.hpp"
|
||||||
|
|
||||||
#include <limits>
|
|
||||||
#include <iomanip>
|
|
||||||
|
|
||||||
#include <components/misc/constants.hpp>
|
#include <components/misc/constants.hpp>
|
||||||
#include <components/misc/rng.hpp>
|
#include <components/misc/rng.hpp>
|
||||||
#include <components/settings/settings.hpp>
|
#include <components/settings/settings.hpp>
|
||||||
@ -29,185 +26,11 @@
|
|||||||
#include "weapontype.hpp"
|
#include "weapontype.hpp"
|
||||||
#include "summoning.hpp"
|
#include "summoning.hpp"
|
||||||
#include "spellresistance.hpp"
|
#include "spellresistance.hpp"
|
||||||
|
#include "spellutil.hpp"
|
||||||
|
#include "tickableeffects.hpp"
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
ESM::Skill::SkillEnum spellSchoolToSkill(int school)
|
|
||||||
{
|
|
||||||
static const std::array<ESM::Skill::SkillEnum, 6> 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<ESM::MagicEffect>().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<ESM::GameSetting>().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<float>::max();
|
|
||||||
float lowestSkill = 0;
|
|
||||||
|
|
||||||
for (const ESM::ENAMstruct& effect : spell->mEffects.mList)
|
|
||||||
{
|
|
||||||
float x = static_cast<float>(effect.mDuration);
|
|
||||||
const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().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<ESM::GameSetting>().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<ESM::Spell>().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<ESM::Spell>().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<ESM::Creature>()->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
|
class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -1016,232 +839,4 @@ namespace MWMechanics
|
|||||||
{
|
{
|
||||||
return !mManualSpell && MWMechanics::spellIncreasesSkill(mId);
|
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<int>((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<int>(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<float> 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; i<sizeof(priorities)/sizeof(int); ++i)
|
|
||||||
{
|
|
||||||
if (disintegrateSlot(actor, priorities[i], magnitude))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESM::MagicEffect::DisintegrateWeapon:
|
|
||||||
disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESM::MagicEffect::SunDamage:
|
|
||||||
{
|
|
||||||
// isInCell shouldn't be needed, but updateActor called during game start
|
|
||||||
if (!actor.isInCell() || !actor.getCell()->isExterior())
|
|
||||||
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<ESM::GameSetting>().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<ESM::MagicEffect>().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<ESM::Static>().find (magicEffect->mHit);
|
|
||||||
else
|
|
||||||
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,10 @@
|
|||||||
#define MWMECHANICS_SPELLCASTING_H
|
#define MWMECHANICS_SPELLCASTING_H
|
||||||
|
|
||||||
#include <components/esm/effectlist.hpp>
|
#include <components/esm/effectlist.hpp>
|
||||||
#include <components/esm/loadskil.hpp>
|
|
||||||
#include <components/esm/loadmgef.hpp>
|
#include <components/esm/loadmgef.hpp>
|
||||||
|
|
||||||
#include "../mwworld/ptr.hpp"
|
#include "../mwworld/ptr.hpp"
|
||||||
|
|
||||||
#include "magiceffects.hpp"
|
|
||||||
|
|
||||||
namespace ESM
|
namespace ESM
|
||||||
{
|
{
|
||||||
struct Spell;
|
struct Spell;
|
||||||
@ -23,38 +20,6 @@ namespace MWMechanics
|
|||||||
class MagicEffects;
|
class MagicEffects;
|
||||||
class CreatureStats;
|
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
|
class CastSpell
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@ -105,22 +70,6 @@ namespace MWMechanics
|
|||||||
/// @return was the target suitable for the effect?
|
/// @return was the target suitable for the effect?
|
||||||
bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude);
|
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
|
#endif
|
||||||
|
@ -15,11 +15,11 @@
|
|||||||
#include "../mwworld/cellstore.hpp"
|
#include "../mwworld/cellstore.hpp"
|
||||||
|
|
||||||
#include "creaturestats.hpp"
|
#include "creaturestats.hpp"
|
||||||
#include "spellcasting.hpp"
|
|
||||||
#include "spellresistance.hpp"
|
#include "spellresistance.hpp"
|
||||||
#include "weapontype.hpp"
|
#include "weapontype.hpp"
|
||||||
#include "combat.hpp"
|
#include "combat.hpp"
|
||||||
#include "summoning.hpp"
|
#include "summoning.hpp"
|
||||||
|
#include "spellutil.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
|
|
||||||
#include "creaturestats.hpp"
|
#include "creaturestats.hpp"
|
||||||
#include "spellcasting.hpp"
|
#include "spellutil.hpp"
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
|
208
apps/openmw/mwmechanics/spellutil.cpp
Normal file
208
apps/openmw/mwmechanics/spellutil.cpp
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#include "spellutil.hpp"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#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<ESM::Skill::SkillEnum, 6> 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<ESM::MagicEffect>().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<ESM::GameSetting>().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<int>((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<float>::max();
|
||||||
|
float lowestSkill = 0;
|
||||||
|
|
||||||
|
for (const ESM::ENAMstruct& effect : spell->mEffects.mList)
|
||||||
|
{
|
||||||
|
float x = static_cast<float>(effect.mDuration);
|
||||||
|
const auto magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().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<ESM::GameSetting>().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<ESM::Spell>().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<ESM::Spell>().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<ESM::Creature>()->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;
|
||||||
|
}
|
||||||
|
}
|
50
apps/openmw/mwmechanics/spellutil.hpp
Normal file
50
apps/openmw/mwmechanics/spellutil.hpp
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#ifndef MWMECHANICS_SPELLUTIL_H
|
||||||
|
#define MWMECHANICS_SPELLUTIL_H
|
||||||
|
|
||||||
|
#include <components/esm/loadskil.hpp>
|
||||||
|
|
||||||
|
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
|
218
apps/openmw/mwmechanics/tickableeffects.cpp
Normal file
218
apps/openmw/mwmechanics/tickableeffects.cpp
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
#include "tickableeffects.hpp"
|
||||||
|
|
||||||
|
#include <components/settings/settings.hpp>
|
||||||
|
|
||||||
|
#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<float> 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<int>(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; i<sizeof(priorities)/sizeof(int); ++i)
|
||||||
|
{
|
||||||
|
if (disintegrateSlot(actor, priorities[i], magnitude))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ESM::MagicEffect::DisintegrateWeapon:
|
||||||
|
disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESM::MagicEffect::SunDamage:
|
||||||
|
{
|
||||||
|
// isInCell shouldn't be needed, but updateActor called during game start
|
||||||
|
if (!actor.isInCell() || !actor.getCell()->isExterior())
|
||||||
|
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<ESM::GameSetting>().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;
|
||||||
|
}
|
||||||
|
}
|
19
apps/openmw/mwmechanics/tickableeffects.hpp
Normal file
19
apps/openmw/mwmechanics/tickableeffects.hpp
Normal file
@ -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
|
@ -13,7 +13,7 @@
|
|||||||
#include "combat.hpp"
|
#include "combat.hpp"
|
||||||
#include "aicombataction.hpp"
|
#include "aicombataction.hpp"
|
||||||
#include "spellpriority.hpp"
|
#include "spellpriority.hpp"
|
||||||
#include "spellcasting.hpp"
|
#include "spellutil.hpp"
|
||||||
#include "weapontype.hpp"
|
#include "weapontype.hpp"
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
#include "../mwbase/mechanicsmanager.hpp"
|
#include "../mwbase/mechanicsmanager.hpp"
|
||||||
|
|
||||||
#include "../mwmechanics/npcstats.hpp"
|
#include "../mwmechanics/npcstats.hpp"
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
|
||||||
#include "../mwmechanics/spellresistance.hpp"
|
#include "../mwmechanics/spellresistance.hpp"
|
||||||
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
#include "../mwmechanics/actorutil.hpp"
|
#include "../mwmechanics/actorutil.hpp"
|
||||||
#include "../mwmechanics/weapontype.hpp"
|
#include "../mwmechanics/weapontype.hpp"
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#include "../mwmechanics/movement.hpp"
|
#include "../mwmechanics/movement.hpp"
|
||||||
#include "../mwmechanics/npcstats.hpp"
|
#include "../mwmechanics/npcstats.hpp"
|
||||||
#include "../mwmechanics/spellcasting.hpp"
|
#include "../mwmechanics/spellutil.hpp"
|
||||||
|
|
||||||
#include "class.hpp"
|
#include "class.hpp"
|
||||||
#include "ptr.hpp"
|
#include "ptr.hpp"
|
||||||
|
@ -3161,12 +3161,42 @@ namespace MWWorld
|
|||||||
mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection);
|
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<ESM::MagicEffect>().find(key.mId);
|
||||||
|
if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0)
|
||||||
|
return;
|
||||||
|
const ESM::Static* castStatic;
|
||||||
|
if (!magicEffect->mHit.empty())
|
||||||
|
castStatic = store.get<ESM::Static>().find (magicEffect->mHit);
|
||||||
|
else
|
||||||
|
castStatic = store.get<ESM::Static>().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)
|
void World::applyLoopingParticles(const MWWorld::Ptr& ptr)
|
||||||
{
|
{
|
||||||
const MWWorld::Class &cls = ptr.getClass();
|
const MWWorld::Class &cls = ptr.getClass();
|
||||||
if (cls.isActor())
|
if (cls.isActor())
|
||||||
{
|
{
|
||||||
MWMechanics::ApplyLoopingParticlesVisitor visitor(ptr);
|
ApplyLoopingParticlesVisitor visitor(ptr);
|
||||||
cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor);
|
cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor);
|
||||||
cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor);
|
cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor);
|
||||||
if (cls.hasInventoryStore(ptr))
|
if (cls.hasInventoryStore(ptr))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user