1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-29 18:32:36 +00:00
OpenMW/apps/openmw/mwmechanics/spellpriority.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

748 lines
30 KiB
C++
Raw Normal View History

#include "spellpriority.hpp"
#include "weaponpriority.hpp"
#include <components/esm3/loadench.hpp>
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadrace.hpp>
#include <components/esm3/loadspel.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
2018-10-08 17:06:30 +03:00
#include "creaturestats.hpp"
2020-04-04 18:28:53 +03:00
#include "spellresistance.hpp"
#include "spellutil.hpp"
2020-04-04 17:45:03 +03:00
#include "summoning.hpp"
#include "weapontype.hpp"
namespace
{
int numEffectsToDispel(const MWWorld::Ptr& actor, int effectFilter = -1, bool negative = true)
{
int toCure = 0;
const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells();
for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it)
{
// if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted
// items etc.
if (effectFilter == -1)
{
const ESM::Spell* spell
2023-04-20 21:07:53 +02:00
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(it->getId());
if (!spell || spell->mData.mType != ESM::Spell::ST_Spell)
continue;
}
const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it;
for (const auto& effect : params.getEffects())
{
int effectId = effect.mEffectId;
if (effectFilter != -1 && effectId != effectFilter)
continue;
const ESM::MagicEffect* magicEffect
2023-04-20 21:07:53 +02:00
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effectId);
if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway
continue;
if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
++toCure;
if (!negative && !(magicEffect->mData.mFlags & ESM::MagicEffect::Harmful))
++toCure;
}
}
return toCure;
}
2017-08-01 21:42:49 +04:00
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
float getSpellDuration(const MWWorld::Ptr& actor, const ESM::RefId& spellId)
2017-08-01 21:42:49 +04:00
{
float duration = 0;
const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells();
for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it)
{
if (it->getId() != spellId)
2017-08-01 21:42:49 +04:00
continue;
const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it;
for (const auto& effect : params.getEffects())
2017-08-01 21:42:49 +04:00
{
if (effect.mDuration > duration)
duration = effect.mDuration;
2017-08-01 21:42:49 +04:00
}
}
return duration;
}
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
bool isSpellActive(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const ESM::RefId& id)
{
int actorId = caster.getClass().getCreatureStats(caster).getActorId();
const auto& active = target.getClass().getCreatureStats(target).getActiveSpells();
return std::find_if(active.begin(), active.end(), [&](const auto& spell) {
return spell.getCasterActorId() == actorId && spell.getId() == id;
}) != active.end();
}
}
namespace MWMechanics
{
int getRangeTypes(const ESM::EffectList& effects)
{
int types = 0;
for (std::vector<ESM::ENAMstruct>::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it)
{
if (it->mRange == ESM::RT_Self)
types |= RangeTypes::Self;
else if (it->mRange == ESM::RT_Touch)
types |= RangeTypes::Touch;
else if (it->mRange == ESM::RT_Target)
types |= RangeTypes::Target;
}
return types;
}
float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor)
{
if (item.getType() != ESM::Potion::sRecordId)
return 0.f;
const ESM::Potion* potion = item.get<ESM::Potion>()->mBase;
return rateEffects(potion->mEffects, actor, MWWorld::Ptr());
}
float rateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
{
float successChance = MWMechanics::getSpellSuccessChance(spell, actor);
if (successChance == 0.f)
return 0.f;
if (spell->mData.mType != ESM::Spell::ST_Spell)
return 0.f;
// Don't make use of racial bonus spells, like MW. Can be made optional later
if (actor.getClass().isNpc())
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::RefId& raceid = actor.get<ESM::NPC>()->mBase->mRace;
2023-04-20 21:07:53 +02:00
const ESM::Race* race = MWBase::Environment::get().getESMStore()->get<ESM::Race>().find(raceid);
if (race->mPowers.exists(spell->mId))
return 0.f;
}
// Spells don't stack, so early out if the spell is still active on the target
int types = getRangeTypes(spell->mEffects);
if ((types & Self) && isSpellActive(actor, actor, spell->mId))
return 0.f;
if (((types & Touch) || (types & Target)) && isSpellActive(actor, enemy, spell->mId))
return 0.f;
return rateEffects(spell->mEffects, actor, enemy) * (successChance / 100.f);
}
float rateMagicItem(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
{
if (ptr.getClass().getEnchantment(ptr).empty())
return 0.f;
2023-04-20 21:07:53 +02:00
const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get<ESM::Enchantment>().find(
ptr.getClass().getEnchantment(ptr));
2017-08-01 21:42:49 +04:00
// Spells don't stack, so early out if the spell is still active on the target
int types = getRangeTypes(enchantment->mEffects);
if ((types & Self)
&& actor.getClass().getCreatureStats(actor).getActiveSpells().isSpellActive(ptr.getCellRef().getRefId()))
return 0.f;
if (types & (Touch | Target) && getSpellDuration(enemy, ptr.getCellRef().getRefId()) > 3)
return 0.f;
if (enchantment->mData.mType == ESM::Enchantment::CastOnce)
{
return rateEffects(enchantment->mEffects, actor, enemy);
}
2017-08-01 21:42:49 +04:00
else if (enchantment->mData.mType == ESM::Enchantment::WhenUsed)
{
2017-08-01 21:42:49 +04:00
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
// Creatures can not wear armor/clothing, so allow creatures to use non-equipped items,
if (actor.getClass().isNpc() && !store.isEquipped(ptr))
return 0.f;
int castCost = getEffectiveEnchantmentCastCost(*enchantment, actor);
2017-08-01 21:42:49 +04:00
if (ptr.getCellRef().getEnchantmentCharge() != -1 && ptr.getCellRef().getEnchantmentCharge() < castCost)
return 0.f;
float rating = rateEffects(enchantment->mEffects, actor, enemy);
2023-05-09 20:07:08 -04:00
rating *= 1.25f; // prefer rechargeable magic items over spells
2017-08-01 21:42:49 +04:00
return rating;
}
2017-08-01 21:42:49 +04:00
return 0.f;
}
float rateEffect(const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
{
// NOTE: enemy may be empty
float rating = 1;
switch (effect.mEffectID)
{
case ESM::MagicEffect::Soultrap:
case ESM::MagicEffect::AlmsiviIntervention:
case ESM::MagicEffect::DivineIntervention:
case ESM::MagicEffect::CalmHumanoid:
case ESM::MagicEffect::CalmCreature:
case ESM::MagicEffect::FrenzyHumanoid:
case ESM::MagicEffect::FrenzyCreature:
case ESM::MagicEffect::DemoralizeHumanoid:
case ESM::MagicEffect::DemoralizeCreature:
case ESM::MagicEffect::RallyHumanoid:
case ESM::MagicEffect::RallyCreature:
case ESM::MagicEffect::Charm:
case ESM::MagicEffect::DetectAnimal:
case ESM::MagicEffect::DetectEnchantment:
case ESM::MagicEffect::DetectKey:
case ESM::MagicEffect::Telekinesis:
case ESM::MagicEffect::Mark:
case ESM::MagicEffect::Recall:
case ESM::MagicEffect::Jump:
case ESM::MagicEffect::WaterBreathing:
case ESM::MagicEffect::SwiftSwim:
case ESM::MagicEffect::WaterWalking:
case ESM::MagicEffect::SlowFall:
case ESM::MagicEffect::Light:
case ESM::MagicEffect::Lock:
case ESM::MagicEffect::Open:
case ESM::MagicEffect::TurnUndead:
case ESM::MagicEffect::WeaknessToCommonDisease:
case ESM::MagicEffect::WeaknessToBlightDisease:
case ESM::MagicEffect::WeaknessToCorprusDisease:
case ESM::MagicEffect::CureCommonDisease:
case ESM::MagicEffect::CureBlightDisease:
case ESM::MagicEffect::CureCorprusDisease:
case ESM::MagicEffect::ResistBlightDisease:
case ESM::MagicEffect::ResistCommonDisease:
case ESM::MagicEffect::ResistCorprusDisease:
case ESM::MagicEffect::Invisibility:
case ESM::MagicEffect::Chameleon:
case ESM::MagicEffect::NightEye:
case ESM::MagicEffect::Vampirism:
case ESM::MagicEffect::StuntedMagicka:
case ESM::MagicEffect::ExtraSpell:
case ESM::MagicEffect::RemoveCurse:
case ESM::MagicEffect::CommandCreature:
case ESM::MagicEffect::CommandHumanoid:
return 0.f;
case ESM::MagicEffect::Blind:
{
if (enemy.isEmpty())
return 0.f;
const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy);
// Enemy can't attack
2018-09-01 01:44:29 +03:00
if (stats.isParalyzed() || stats.getKnockedDown())
return 0.f;
// Enemy doesn't attack
if (stats.getDrawState() != MWMechanics::DrawState::Weapon)
return 0.f;
break;
}
case ESM::MagicEffect::Sound:
{
if (enemy.isEmpty())
return 0.f;
const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy);
// Enemy can't cast spells
2023-05-23 19:06:08 +02:00
if (stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Silence).getMagnitude() > 0)
return 0.f;
2018-09-01 01:44:29 +03:00
if (stats.isParalyzed() || stats.getKnockedDown())
return 0.f;
// Enemy doesn't cast spells
if (stats.getDrawState() != MWMechanics::DrawState::Spell)
return 0.f;
break;
}
case ESM::MagicEffect::Silence:
{
if (enemy.isEmpty())
return 0.f;
const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy);
// Enemy can't cast spells
2018-09-01 01:44:29 +03:00
if (stats.isParalyzed() || stats.getKnockedDown())
return 0.f;
// Enemy doesn't cast spells
if (stats.getDrawState() != MWMechanics::DrawState::Spell)
return 0.f;
break;
}
case ESM::MagicEffect::RestoreAttribute:
return 0.f; // TODO: implement based on attribute damage
case ESM::MagicEffect::RestoreSkill:
return 0.f; // TODO: implement based on skill damage
2022-09-22 21:26:05 +03:00
case ESM::MagicEffect::ResistFire:
case ESM::MagicEffect::ResistFrost:
case ESM::MagicEffect::ResistMagicka:
case ESM::MagicEffect::ResistNormalWeapons:
case ESM::MagicEffect::ResistParalysis:
case ESM::MagicEffect::ResistPoison:
case ESM::MagicEffect::ResistShock:
case ESM::MagicEffect::SpellAbsorption:
case ESM::MagicEffect::Reflect:
return 0.f; // probably useless since we don't know in advance what the enemy will cast
2022-09-22 21:26:05 +03:00
// don't cast these for now as they would make the NPC cast the same effect over and over again, especially
// when they have potions
case ESM::MagicEffect::FortifyAttribute:
case ESM::MagicEffect::FortifyHealth:
case ESM::MagicEffect::FortifyMagicka:
case ESM::MagicEffect::FortifyFatigue:
case ESM::MagicEffect::FortifySkill:
case ESM::MagicEffect::FortifyMaximumMagicka:
case ESM::MagicEffect::FortifyAttack:
return 0.f;
case ESM::MagicEffect::Burden:
{
if (enemy.isEmpty())
return 0.f;
// Ignore enemy without inventory
if (!enemy.getClass().hasInventoryStore(enemy))
return 0.f;
// burden makes sense only to overburden an enemy
float burden = enemy.getClass().getEncumbrance(enemy) - enemy.getClass().getCapacity(enemy);
if (burden > 0)
return 0.f;
if ((effect.mMagnMin + effect.mMagnMax) / 2.f > -burden)
rating *= 3;
else
return 0.f;
break;
}
case ESM::MagicEffect::Feather:
{
// Ignore actors without inventory
if (!actor.getClass().hasInventoryStore(actor))
return 0.f;
// feather makes sense only for overburden actors
float burden = actor.getClass().getEncumbrance(actor) - actor.getClass().getCapacity(actor);
if (burden <= 0)
return 0.f;
if ((effect.mMagnMin + effect.mMagnMax) / 2.f >= burden)
rating *= 3;
else
return 0.f;
break;
}
case ESM::MagicEffect::Levitate:
return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway
case ESM::MagicEffect::BoundBoots:
case ESM::MagicEffect::BoundHelm:
if (actor.getClass().isNpc())
{
// Beast races can't wear helmets or boots
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::RefId& raceid = actor.get<ESM::NPC>()->mBase->mRace;
2023-04-20 21:07:53 +02:00
const ESM::Race* race = MWBase::Environment::get().getESMStore()->get<ESM::Race>().find(raceid);
if (race->mData.mFlags & ESM::Race::Beast)
2021-11-08 17:28:54 +01:00
return 0.f;
2022-09-22 21:26:05 +03:00
}
else
return 0.f;
2018-06-28 11:12:48 +04:00
break;
2021-11-07 22:13:27 +01:00
case ESM::MagicEffect::BoundShield:
2021-11-08 17:28:54 +01:00
if (!actor.getClass().hasInventoryStore(actor))
return 0.f;
else if (!actor.getClass().isNpc())
2021-11-07 22:13:27 +01:00
{
// If the actor is an NPC they can benefit from the armor rating, otherwise check if we've got a
// one-handed weapon to use with the shield
2021-11-08 17:28:54 +01:00
const auto& store = actor.getClass().getInventoryStore(actor);
auto oneHanded = std::find_if(store.cbegin(MWWorld::ContainerStore::Type_Weapon), store.cend(),
[](const MWWorld::ConstPtr& weapon) {
if (weapon.getClass().getItemHealth(weapon) <= 0.f)
return false;
short type = weapon.get<ESM::Weapon>()->mBase->mData.mType;
return !(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded);
});
if (oneHanded == store.cend())
return 0.f;
2022-09-22 21:26:05 +03:00
}
break;
// Creatures can not wear armor
case ESM::MagicEffect::BoundCuirass:
case ESM::MagicEffect::BoundGloves:
if (!actor.getClass().isNpc())
2021-11-08 17:28:54 +01:00
return 0.f;
2021-11-07 22:13:27 +01:00
break;
case ESM::MagicEffect::RestoreHealth:
case ESM::MagicEffect::RestoreMagicka:
case ESM::MagicEffect::RestoreFatigue:
if (effect.mRange == ESM::RT_Self)
{
int priority = 1;
if (effect.mEffectID == ESM::MagicEffect::RestoreHealth)
priority = 10;
const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
const DynamicStat<float>& current
= stats.getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth);
// NB: this currently assumes the hardcoded magic effect flags are used
const float magnitude = (effect.mMagnMin + effect.mMagnMax) / 2.f;
const float toHeal = magnitude * std::max(1, effect.mDuration);
// Effect doesn't heal more than we need, *or* we are below 1/2 health
if (current.getModified() - current.getCurrent() > toHeal
|| current.getCurrent() < current.getModified() * 0.5)
2022-09-22 21:26:05 +03:00
{
return 10000.f * priority
- (toHeal
- (current.getModified() - current.getCurrent())); // prefer the most fitting potion
2022-09-22 21:26:05 +03:00
}
else
2018-08-31 00:36:30 +03:00
return -10000.f * priority; // Save for later
}
break;
case ESM::MagicEffect::Dispel:
{
int numPositive = 0;
int numNegative = 0;
int diff = 0;
if (effect.mRange == ESM::RT_Self)
2022-09-22 21:26:05 +03:00
{
numPositive = numEffectsToDispel(actor, -1, false);
numNegative = numEffectsToDispel(actor);
diff = numNegative - numPositive;
2022-09-22 21:26:05 +03:00
}
else
2022-09-22 21:26:05 +03:00
{
if (enemy.isEmpty())
return 0.f;
numPositive = numEffectsToDispel(enemy, -1, false);
numNegative = numEffectsToDispel(enemy);
diff = numPositive - numNegative;
// if rating < 0 here, the spell will be considered as negative later
rating *= -1;
2022-09-22 21:26:05 +03:00
}
if (diff <= 0)
return 0.f;
rating *= (diff) / 5.f;
2022-09-22 21:26:05 +03:00
break;
}
// Prefer Cure effects over Dispel, because Dispel also removes positive effects
case ESM::MagicEffect::CureParalyzation:
return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Paralyze);
case ESM::MagicEffect::CurePoison:
return 1001.f * numEffectsToDispel(actor, ESM::MagicEffect::Poison);
case ESM::MagicEffect::DisintegrateArmor:
{
if (enemy.isEmpty())
return 0.f;
// Ignore enemy without inventory
if (!enemy.getClass().hasInventoryStore(enemy))
return 0.f;
MWWorld::InventoryStore& inv = enemy.getClass().getInventoryStore(enemy);
// According to UESP
static const int armorSlots[] = {
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,
};
bool enemyHasArmor = false;
// Ignore enemy without armor
for (unsigned int i = 0; i < sizeof(armorSlots) / sizeof(int); ++i)
{
MWWorld::ContainerStoreIterator item = inv.getSlot(armorSlots[i]);
if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor))
{
enemyHasArmor = true;
break;
}
}
if (!enemyHasArmor)
return 0.f;
break;
}
case ESM::MagicEffect::DisintegrateWeapon:
{
if (enemy.isEmpty())
return 0.f;
// Ignore enemy without inventory
if (!enemy.getClass().hasInventoryStore(enemy))
return 0.f;
MWWorld::InventoryStore& inv = enemy.getClass().getInventoryStore(enemy);
MWWorld::ContainerStoreIterator item = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
// Ignore enemy without weapons
if (item == inv.end() || (item.getType() != MWWorld::ContainerStore::Type_Weapon))
return 0.f;
break;
}
case ESM::MagicEffect::AbsorbAttribute:
case ESM::MagicEffect::DamageAttribute:
case ESM::MagicEffect::DrainAttribute:
if (!enemy.isEmpty()
2023-06-19 20:41:54 +02:00
&& enemy.getClass()
.getCreatureStats(enemy)
.getAttribute(ESM::Attribute::AttributeID(effect.mAttribute))
.getModified()
<= 0)
return 0.f;
{
if (effect.mAttribute >= 0 && effect.mAttribute < ESM::Attribute::Length)
2022-09-22 21:26:05 +03:00
{
const float attributePriorities[ESM::Attribute::Length] = {
1.0f, // Strength
0.5f, // Intelligence
0.6f, // Willpower
0.7f, // Agility
0.5f, // Speed
0.8f, // Endurance
0.7f, // Personality
0.3f // Luck
};
rating *= attributePriorities[effect.mAttribute];
2022-09-22 21:26:05 +03:00
}
}
break;
case ESM::MagicEffect::AbsorbSkill:
case ESM::MagicEffect::DamageSkill:
case ESM::MagicEffect::DrainSkill:
if (enemy.isEmpty() || !enemy.getClass().isNpc())
return 0.f;
2023-06-05 21:21:30 +02:00
if (enemy.getClass().getSkill(enemy, ESM::Skill::indexToRefId(effect.mSkill)) <= 0)
return 0.f;
break;
default:
break;
}
// Allow only one summoned creature at time
if (isSummoningEffect(effect.mEffectID))
{
MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor);
if (!creatureStats.getSummonedCreatureMap().empty())
return 0.f;
// But rate summons higher than other effects
rating = 3.f;
}
2021-11-07 22:13:27 +01:00
if (effect.mEffectID >= ESM::MagicEffect::BoundDagger && effect.mEffectID <= ESM::MagicEffect::BoundGloves)
{
// Prefer casting bound items over other spells
rating = 2.f;
2021-11-07 22:13:27 +01:00
// While rateSpell prevents actors from recasting the same spell, it doesn't prevent them from casting
// different spells with the same effect. Multiple instances of the same bound item don't stack so if the
// effect is already active, rate it as useless. Likewise, if the actor already has a bound weapon, don't
// summon another of a different kind unless what we have is a bow and the actor is out of ammo.
// FIXME: This code assumes the summoned item is of the usual type (i.e. a mod hasn't changed Bound Bow to
// summon an Axe instead)
if (effect.mEffectID <= ESM::MagicEffect::BoundLongbow)
{
for (int e = ESM::MagicEffect::BoundDagger; e <= ESM::MagicEffect::BoundLongbow; ++e)
2023-05-23 19:06:08 +02:00
if (actor.getClass().getCreatureStats(actor).getMagicEffects().getOrDefault(e).getMagnitude() > 0.f
&& (e != ESM::MagicEffect::BoundLongbow || effect.mEffectID == e
|| rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f))
return 0.f;
2023-06-06 17:24:22 +02:00
ESM::RefId skill = ESM::Skill::ShortBlade;
if (effect.mEffectID == ESM::MagicEffect::BoundLongsword)
skill = ESM::Skill::LongBlade;
else if (effect.mEffectID == ESM::MagicEffect::BoundMace)
skill = ESM::Skill::BluntWeapon;
else if (effect.mEffectID == ESM::MagicEffect::BoundBattleAxe)
skill = ESM::Skill::Axe;
else if (effect.mEffectID == ESM::MagicEffect::BoundSpear)
skill = ESM::Skill::Spear;
else if (effect.mEffectID == ESM::MagicEffect::BoundLongbow)
{
// AI should not summon the bow if there is no suitable ammo.
if (rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f)
return 0.f;
skill = ESM::Skill::Marksman;
}
// Prefer summoning items we know how to use
rating *= (50.f + actor.getClass().getSkill(actor, skill)) / 100.f;
}
2023-05-23 19:06:08 +02:00
else if (actor.getClass()
.getCreatureStats(actor)
.getMagicEffects()
.getOrDefault(effect.mEffectID)
.getMagnitude()
> 0.f)
2021-11-07 22:13:27 +01:00
return 0.f;
}
// Underwater casting not possible
if (effect.mRange == ESM::RT_Target)
{
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.75f))
return 0.f;
if (enemy.isEmpty())
return 0.f;
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(enemy), 0.75f))
return 0.f;
}
2018-08-27 13:38:53 +04:00
const ESM::MagicEffect* magicEffect
2023-04-20 21:07:53 +02:00
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mEffectID);
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
{
rating *= -1.f;
if (enemy.isEmpty())
return 0.f;
// Check resistance for harmful effects
CreatureStats& stats = enemy.getClass().getCreatureStats(enemy);
float resistance = MWMechanics::getEffectResistanceAttribute(effect.mEffectID, &stats.getMagicEffects());
rating *= (1.f - std::min(resistance, 100.f) / 100.f);
}
// for harmful no-magnitude effects (e.g. silence) check if enemy is already has them
// for non-harmful no-magnitude effects (e.g. bound items) check if actor is already has them
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
{
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
{
CreatureStats& stats = enemy.getClass().getCreatureStats(enemy);
2023-05-23 19:06:08 +02:00
if (stats.getMagicEffects().getOrDefault(effect.mEffectID).getMagnitude() > 0)
return 0.f;
}
else
{
CreatureStats& stats = actor.getClass().getCreatureStats(actor);
2023-05-23 19:06:08 +02:00
if (stats.getMagicEffects().getOrDefault(effect.mEffectID).getMagnitude() > 0)
return 0.f;
}
}
2018-08-27 13:38:53 +04:00
rating *= calcEffectCost(effect, magicEffect);
// Currently treating all "on target" or "on touch" effects to target the enemy actor.
// Combat AI is egoistic, so doesn't consider applying positive effects to friendly actors.
if (effect.mRange != ESM::RT_Self)
rating *= -1.f;
return rating;
}
float rateEffects(
const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool useSpellMult)
{
// NOTE: enemy may be empty
float rating = 0.f;
const MWWorld::Store<ESM::GameSetting>& gmst
2023-04-20 21:07:53 +02:00
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat();
static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat();
for (const ESM::ENAMstruct& effect : list.mList)
{
float effectRating = rateEffect(effect, actor, enemy);
if (useSpellMult)
{
if (effect.mRange == ESM::RT_Target)
effectRating *= fAIRangeMagicSpellMult;
else
effectRating *= fAIMagicSpellMult;
}
rating += effectRating;
}
return rating;
}
float vanillaRateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
{
const MWWorld::Store<ESM::GameSetting>& gmst
2023-04-20 21:07:53 +02:00
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
2018-08-29 18:38:12 +03:00
static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat();
static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat();
float mult = fAIMagicSpellMult;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
effectIt != spell->mEffects.mList.end(); ++effectIt)
{
if (effectIt->mRange == ESM::RT_Target)
{
if (!MWBase::Environment::get().getWorld()->isSwimming(enemy))
mult = fAIRangeMagicSpellMult;
else
mult = 0.0f;
break;
}
}
return MWMechanics::getSpellSuccessChance(spell, actor) * mult;
}
}