You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openmw-tes3mp/apps/openmw/mwmechanics/aicombataction.cpp

590 lines
22 KiB
C++

#include "aicombataction.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "../mwworld/actionequip.hpp"
#include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/spellcasting.hpp"
#include <components/esm/loadench.hpp>
#include <components/esm/loadmgef.hpp>
namespace
{
// RangeTypes using bitflags to allow multiple range types, as can be the case with spells having multiple effects.
enum RangeTypes
{
Self = 0x1,
Touch = 0x10,
Target = 0x100
};
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 |= Self;
else if (it->mRange == ESM::RT_Touch)
types |= Touch;
else if (it->mRange == ESM::RT_Target)
types |= Target;
}
return types;
}
void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow)
{
if (rangeTypes & Touch)
{
rangeAttack = 100.f;
rangeFollow = 300.f;
}
else if (rangeTypes & Target)
{
rangeAttack = 1000.f;
rangeFollow = 0.f;
}
else
{
// For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits
rangeAttack = 600.f;
rangeFollow = 0.f;
}
}
int numEffectsToCure (const MWWorld::Ptr& actor, int effectFilter=-1)
{
int toCure=0;
const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells();
for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it)
{
const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second;
for (std::vector<MWMechanics::ActiveSpells::ActiveEffect>::const_iterator effectIt = params.mEffects.begin();
effectIt != params.mEffects.end(); ++effectIt)
{
int effectId = effectIt->mEffectId;
if (effectFilter != -1 && effectId != effectFilter)
continue;
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effectId);
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful
&& effectIt->mDuration > 3 // Don't attempt to cure if effect runs out shortly anyway
)
++toCure;
}
}
return toCure;
}
}
namespace MWMechanics
{
float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor)
{
if (item.getTypeName() != typeid(ESM::Potion).name())
return 0.f;
const ESM::Potion* potion = item.get<ESM::Potion>()->mBase;
return rateEffects(potion->mEffects, actor, MWWorld::Ptr());
}
float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type,
float arrowRating, float boltRating)
{
if (item.getTypeName() != typeid(ESM::Weapon).name())
return 0.f;
const ESM::Weapon* weapon = item.get<ESM::Weapon>()->mBase;
if (type != -1 && weapon->mData.mType != type)
return 0.f;
float rating=0.f;
if (weapon->mData.mType >= ESM::Weapon::MarksmanBow)
{
rating = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2.f;
}
else
{
for (int i=0; i<2; ++i)
{
rating += weapon->mData.mSlash[i];
rating += weapon->mData.mThrust[i];
rating += weapon->mData.mChop[i];
}
rating /= 6.f;
}
if (item.getClass().hasItemHealth(item))
{
if (item.getClass().getItemHealth(item) == 0)
return 0.f;
rating *= item.getClass().getItemHealth(item) / float(item.getClass().getItemMaxHealth(item));
}
if (weapon->mData.mType == ESM::Weapon::MarksmanBow)
{
if (arrowRating <= 0.f)
rating = 0.f;
else
rating += arrowRating;
}
else if (weapon->mData.mType == ESM::Weapon::MarksmanCrossbow)
{
if (boltRating <= 0.f)
rating = 0.f;
else
rating += boltRating;
}
if (!weapon->mEnchant.empty())
{
const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find(weapon->mEnchant);
if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes
&& (item.getCellRef().getEnchantmentCharge() == -1
|| item.getCellRef().getEnchantmentCharge() >= enchantment->mData.mCost))
rating += rateEffects(enchantment->mEffects, actor, enemy);
}
int skill = item.getClass().getEquipmentSkill(item);
if (skill != -1)
rating *= actor.getClass().getSkill(actor, skill) / 100.f;
return rating;
}
float rateSpell(const ESM::Spell *spell, const MWWorld::Ptr &actor, const MWWorld::Ptr& enemy)
{
const CreatureStats& stats = actor.getClass().getCreatureStats(actor);
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())
{
std::string raceid = actor.get<ESM::NPC>()->mBase->mRace;
const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get<ESM::Race>().find(raceid);
if (race->mPowers.exists(spell->mId))
return 0.f;
}
if (spell->mData.mCost > stats.getMagicka().getCurrent())
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) && stats.getActiveSpells().isSpellActive(spell->mId))
return 0.f;
if ( ((types & Touch) || (types & Target)) && enemy.getClass().getCreatureStats(enemy).getActiveSpells().isSpellActive(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<ESM::Enchantment>().find(ptr.getClass().getEnchantment(ptr));
if (enchantment->mData.mType == ESM::Enchantment::CastOnce)
{
return rateEffects(enchantment->mEffects, actor, enemy);
}
else
{
//if (!ptr.getClass().canBeEquipped(ptr, actor))
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:
return 0.f;
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:
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:
return 0.f;
case ESM::MagicEffect::Feather:
if (actor.getClass().getEncumbrance(actor) - actor.getClass().getCapacity(actor) >= 0)
return 100.f;
else
return 0.f;
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
std::string raceid = actor.get<ESM::NPC>()->mBase->mRace;
const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get<ESM::Race>().find(raceid);
if (race->mData.mFlags & ESM::Race::Beast)
return 0.f;
}
// Intended fall-through
// 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 DynamicStat<float>& current = actor.getClass().getCreatureStats(actor).
getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth);
float toHeal = (effect.mMagnMin + effect.mMagnMax)/2.f * 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;
// Prefer Cure effects over Dispel, because Dispel also removes positive effects
case ESM::MagicEffect::Dispel:
return 1000.f * numEffectsToCure(actor);
case ESM::MagicEffect::CureParalyzation:
return 1001.f * numEffectsToCure(actor, ESM::MagicEffect::Paralyze);
case ESM::MagicEffect::CurePoison:
return 1001.f * numEffectsToCure(actor, ESM::MagicEffect::Poison);
case ESM::MagicEffect::DisintegrateArmor: // TODO: check if actor is wearing armor
case ESM::MagicEffect::DisintegrateWeapon: // TODO: check if actor is wearing weapon
break;
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::DamageSkill:
case ESM::MagicEffect::DrainSkill:
if (enemy.isEmpty() || !enemy.getClass().isNpc())
return 0.f;
if (enemy.getClass().getNpcStats(enemy).getSkill(effect.mSkill).getModified() <= 0)
return 0.f;
break;
default:
break;
}
// TODO: for non-cumulative effects (e.g. paralyze), check if the enemy is already suffering from them
// TODO: could take into account enemy's resistance/weakness against the effect
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effect.mEffectID);
rating *= magicEffect->mData.mBaseCost;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
rating *= (effect.mMagnMin + effect.mMagnMax)/2.f;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
rating *= effect.mDuration;
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
rating *= -1.f;
// 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)
{
// NOTE: enemy may be empty
float rating = 0.f;
for (std::vector<ESM::ENAMstruct>::const_iterator it = list.mList.begin(); it != list.mList.end(); ++it)
{
rating += rateEffect(*it, actor, enemy);
}
return rating;
}
void ActionSpell::prepare(const MWWorld::Ptr &actor)
{
actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId);
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell);
if (actor.getClass().hasInventoryStore(actor))
{
MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor);
inv.setSelectedEnchantItem(inv.end());
}
}
void ActionSpell::getCombatRange(float& rangeAttack, float& rangeFollow)
{
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(mSpellId);
int types = getRangeTypes(spell->mEffects);
suggestCombatRange(types, rangeAttack, rangeFollow);
}
void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor)
{
actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(std::string());
actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem);
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell);
}
void ActionEnchantedItem::getCombatRange(float& rangeAttack, float& rangeFollow)
{
const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find(mItem->getClass().getEnchantment(*mItem));
int types = getRangeTypes(enchantment->mEffects);
suggestCombatRange(types, rangeAttack, rangeFollow);
}
void ActionPotion::getCombatRange(float& rangeAttack, float& rangeFollow)
{
// distance doesn't matter, so back away slightly to avoid enemy hits
rangeAttack = 600.f;
rangeFollow = 0.f;
}
void ActionPotion::prepare(const MWWorld::Ptr &actor)
{
actor.getClass().apply(actor, mPotion.getCellRef().getRefId(), actor);
actor.getClass().getContainerStore(actor).remove(mPotion, 1, actor);
}
void ActionWeapon::prepare(const MWWorld::Ptr &actor)
{
if (actor.getClass().hasInventoryStore(actor))
{
if (mWeapon.isEmpty())
actor.getClass().getInventoryStore(actor).unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor);
else
{
MWWorld::ActionEquip equip(mWeapon);
equip.execute(actor);
}
if (!mAmmunition.isEmpty())
{
MWWorld::ActionEquip equip(mAmmunition);
equip.execute(actor);
}
}
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon);
}
void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow)
{
// Already done in AiCombat itself
}
boost::shared_ptr<Action> prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy)
{
Spells& spells = actor.getClass().getCreatureStats(actor).getSpells();
float bestActionRating = 0.f;
// Default to hand-to-hand combat
boost::shared_ptr<Action> bestAction (new ActionWeapon(MWWorld::Ptr()));
if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf())
{
bestAction->prepare(actor);
return bestAction;
}
if (actor.getClass().hasInventoryStore(actor))
{
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = ratePotion(*it, actor);
if (rating > bestActionRating)
{
bestActionRating = rating;
bestAction.reset(new ActionPotion(*it));
}
}
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = rateMagicItem(*it, actor, enemy);
if (rating > bestActionRating)
{
bestActionRating = rating;
bestAction.reset(new ActionEnchantedItem(it));
}
}
float bestArrowRating = 0;
MWWorld::Ptr bestArrow;
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = rateWeapon(*it, actor, enemy, ESM::Weapon::Arrow);
if (rating > bestArrowRating)
{
bestArrowRating = rating;
bestArrow = *it;
}
}
float bestBoltRating = 0;
MWWorld::Ptr bestBolt;
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
float rating = rateWeapon(*it, actor, enemy, ESM::Weapon::Bolt);
if (rating > bestBoltRating)
{
bestBoltRating = rating;
bestBolt = *it;
}
}
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
{
std::vector<int> equipmentSlots = it->getClass().getEquipmentSlots(*it).first;
if (std::find(equipmentSlots.begin(), equipmentSlots.end(), (int)MWWorld::InventoryStore::Slot_CarriedRight)
== equipmentSlots.end())
continue;
float rating = rateWeapon(*it, actor, enemy, -1, bestArrowRating, bestBoltRating);
if (rating > bestActionRating)
{
const ESM::Weapon* weapon = it->get<ESM::Weapon>()->mBase;
MWWorld::Ptr ammo;
if (weapon->mData.mType == ESM::Weapon::MarksmanBow)
ammo = bestArrow;
else if (weapon->mData.mType == ESM::Weapon::MarksmanCrossbow)
ammo = bestBolt;
bestActionRating = rating;
bestAction.reset(new ActionWeapon(*it, ammo));
}
}
}
for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
{
const ESM::Spell* spell = it->first;
float rating = rateSpell(spell, actor, enemy);
if (rating > bestActionRating)
{
bestActionRating = rating;
bestAction.reset(new ActionSpell(spell->mId));
}
}
if (bestAction.get())
bestAction->prepare(actor);
return bestAction;
}
}