#include "spellpriority.hpp" #include "weaponpriority.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "creaturestats.hpp" #include "spellresistance.hpp" #include "spellutil.hpp" #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 = MWBase::Environment::get().getWorld()->getStore().get().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 = MWBase::Environment::get().getWorld()->getStore().get().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; } float getSpellDuration(const MWWorld::Ptr& actor, const ESM::RefId& spellId) { 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) continue; const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; for (const auto& effect : params.getEffects()) { if (effect.mDuration > duration) duration = effect.mDuration; } } return duration; } 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::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()->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()) { const ESM::RefId& raceid = actor.get()->mBase->mRace; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().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; const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( ptr.getClass().getEnchantment(ptr)); // 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); } else if (enchantment->mData.mType == ESM::Enchantment::WhenUsed) { 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(static_cast(enchantment->mData.mCost), actor); if (ptr.getCellRef().getEnchantmentCharge() != -1 && ptr.getCellRef().getEnchantmentCharge() < castCost) return 0.f; float rating = rateEffects(enchantment->mEffects, actor, enemy); rating *= 1.25f; // prefer rechargable magic items over spells return rating; } 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 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 if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude() > 0) return 0.f; 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 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 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 // 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 const ESM::RefId& raceid = actor.get()->mBase->mRace; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); if (race->mData.mFlags & ESM::Race::Beast) return 0.f; } else return 0.f; break; case ESM::MagicEffect::BoundShield: if (!actor.getClass().hasInventoryStore(actor)) return 0.f; else if (!actor.getClass().isNpc()) { // 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 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()->mBase->mData.mType; return !(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded); }); if (oneHanded == store.cend()) return 0.f; } break; // Creatures can not wear armor case ESM::MagicEffect::BoundCuirass: case ESM::MagicEffect::BoundGloves: if (!actor.getClass().isNpc()) return 0.f; 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& 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) { return 10000.f * priority - (toHeal - (current.getModified() - current.getCurrent())); // prefer the most fitting potion } else 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) { numPositive = numEffectsToDispel(actor, -1, false); numNegative = numEffectsToDispel(actor); diff = numNegative - numPositive; } else { 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; } if (diff <= 0) return 0.f; rating *= (diff) / 5.f; 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() && enemy.getClass().getCreatureStats(enemy).getAttribute(effect.mAttribute).getModified() <= 0) return 0.f; { if (effect.mAttribute >= 0 && effect.mAttribute < ESM::Attribute::Length) { 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]; } } break; case ESM::MagicEffect::AbsorbSkill: case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::DrainSkill: if (enemy.isEmpty() || !enemy.getClass().isNpc()) return 0.f; if (enemy.getClass().getSkill(enemy, 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; } if (effect.mEffectID >= ESM::MagicEffect::BoundDagger && effect.mEffectID <= ESM::MagicEffect::BoundGloves) { // Prefer casting bound items over other spells rating = 2.f; // 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) if (actor.getClass().getCreatureStats(actor).getMagicEffects().get(e).getMagnitude() > 0.f && (e != ESM::MagicEffect::BoundLongbow || effect.mEffectID == e || rateAmmo(actor, enemy, getWeaponType(ESM::Weapon::MarksmanBow)->mAmmoType) <= 0.f)) return 0.f; ESM::Skill::SkillEnum 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; } else if (actor.getClass().getCreatureStats(actor).getMagicEffects().get(effect.mEffectID).getMagnitude() > 0.f) 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; } const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().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); if (stats.getMagicEffects().get(effect.mEffectID).getMagnitude() > 0) return 0.f; } else { CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.getMagicEffects().get(effect.mEffectID).getMagnitude() > 0) return 0.f; } } 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& gmst = MWBase::Environment::get().getWorld()->getStore().get(); 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& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); float mult = fAIMagicSpellMult; for (std::vector::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; } }