mirror of
https://github.com/OpenMW/openmw.git
synced 2025-02-28 11:39:42 +00:00
Tweak AI rating to reduce healing spam
This commit is contained in:
parent
e4a6f70011
commit
7c82405c9f
9 changed files with 138 additions and 50 deletions
|
@ -5,6 +5,7 @@
|
|||
Bug #3842: Body part skeletons override the main skeleton
|
||||
Bug #4127: Weapon animation looks choppy
|
||||
Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game
|
||||
Bug #4207: RestoreHealth/Fatigue spells have a huge priority even if a success chance is near 0
|
||||
Bug #4382: Sound output device does not change when it should
|
||||
Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely
|
||||
Bug #4754: Stack of ammunition cannot be equipped partially
|
||||
|
|
|
@ -153,6 +153,7 @@ namespace MWMechanics
|
|||
updateFleeing(actor, target, duration, characterController.getSupportedMovementDirections(), storage);
|
||||
}
|
||||
storage.mActionCooldown -= duration;
|
||||
storage.mHealCooldown -= duration;
|
||||
|
||||
if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting)
|
||||
return false;
|
||||
|
@ -176,6 +177,7 @@ namespace MWMechanics
|
|||
storage.stopAttack();
|
||||
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false);
|
||||
storage.mActionCooldown = 0.f;
|
||||
storage.mHealCooldown = 0.f;
|
||||
// Continue combat if target is player or player follower/escorter and an attack has been attempted
|
||||
const auto& playerFollowersAndEscorters
|
||||
= MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer());
|
||||
|
@ -196,6 +198,7 @@ namespace MWMechanics
|
|||
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
|
||||
|
||||
float& actionCooldown = storage.mActionCooldown;
|
||||
float& healCooldown = storage.mHealCooldown;
|
||||
std::unique_ptr<Action>& currentAction = storage.mCurrentAction;
|
||||
|
||||
if (!forceFlee)
|
||||
|
@ -205,14 +208,16 @@ namespace MWMechanics
|
|||
|
||||
if (characterController.readyToPrepareAttack())
|
||||
{
|
||||
currentAction = prepareNextAction(actor, target);
|
||||
currentAction = prepareNextAction(actor, target, healCooldown <= 0.f);
|
||||
actionCooldown = currentAction->getActionCooldown();
|
||||
healCooldown = currentAction->getHealCooldown();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentAction = std::make_unique<ActionFlee>();
|
||||
actionCooldown = currentAction->getActionCooldown();
|
||||
healCooldown = currentAction->getHealCooldown();
|
||||
}
|
||||
|
||||
if (!currentAction)
|
||||
|
@ -317,6 +322,7 @@ namespace MWMechanics
|
|||
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false);
|
||||
currentAction = std::make_unique<ActionFlee>();
|
||||
actionCooldown = currentAction->getActionCooldown();
|
||||
healCooldown = currentAction->getHealCooldown();
|
||||
storage.startFleeing();
|
||||
MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("flee"));
|
||||
}
|
||||
|
@ -508,6 +514,7 @@ namespace MWMechanics
|
|||
, mCell(nullptr)
|
||||
, mCurrentAction()
|
||||
, mActionCooldown(0.0f)
|
||||
, mHealCooldown(0.f)
|
||||
, mStrength()
|
||||
, mForceNoShortcut(false)
|
||||
, mShortcutFailPos()
|
||||
|
|
|
@ -36,6 +36,7 @@ namespace MWMechanics
|
|||
const MWWorld::CellStore* mCell;
|
||||
std::unique_ptr<Action> mCurrentAction;
|
||||
float mActionCooldown;
|
||||
float mHealCooldown;
|
||||
float mStrength;
|
||||
bool mForceNoShortcut;
|
||||
ESM::Position mShortcutFailPos;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "combat.hpp"
|
||||
#include "npcstats.hpp"
|
||||
#include "spellpriority.hpp"
|
||||
#include "spellutil.hpp"
|
||||
#include "weaponpriority.hpp"
|
||||
#include "weapontype.hpp"
|
||||
|
||||
|
@ -161,7 +162,7 @@ namespace MWMechanics
|
|||
return mWeapon.get<ESM::Weapon>()->mBase;
|
||||
}
|
||||
|
||||
std::unique_ptr<Action> prepareNextAction(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
|
||||
std::unique_ptr<Action> prepareNextAction(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool allowHealing)
|
||||
{
|
||||
Spells& spells = actor.getClass().getCreatureStats(actor).getSpells();
|
||||
|
||||
|
@ -181,23 +182,26 @@ namespace MWMechanics
|
|||
|
||||
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
|
||||
{
|
||||
float rating = ratePotion(*it, actor);
|
||||
if (rating > bestActionRating)
|
||||
bool isPureHealing = false;
|
||||
if (it->getType() == ESM::Potion::sRecordId)
|
||||
{
|
||||
bestActionRating = rating;
|
||||
bestAction = std::make_unique<ActionPotion>(*it);
|
||||
antiFleeRating = std::numeric_limits<float>::max();
|
||||
float rating = ratePotion(*it, actor, isPureHealing);
|
||||
if (rating > bestActionRating && (!isPureHealing || allowHealing))
|
||||
{
|
||||
bestActionRating = rating;
|
||||
bestAction = std::make_unique<ActionPotion>(*it, isPureHealing);
|
||||
antiFleeRating = std::numeric_limits<float>::max();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
|
||||
{
|
||||
float rating = rateMagicItem(*it, actor, enemy);
|
||||
if (rating > bestActionRating)
|
||||
else if (!it->getClass().getEnchantment(*it).empty())
|
||||
{
|
||||
bestActionRating = rating;
|
||||
bestAction = std::make_unique<ActionEnchantedItem>(it);
|
||||
antiFleeRating = std::numeric_limits<float>::max();
|
||||
float rating = rateMagicItem(*it, actor, enemy, isPureHealing);
|
||||
if (rating > bestActionRating && (!isPureHealing || allowHealing))
|
||||
{
|
||||
bestActionRating = rating;
|
||||
bestAction = std::make_unique<ActionEnchantedItem>(it, isPureHealing);
|
||||
antiFleeRating = std::numeric_limits<float>::max();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,8 +237,11 @@ namespace MWMechanics
|
|||
float rating = rateSpell(spell, actor, enemy);
|
||||
if (rating > bestActionRating)
|
||||
{
|
||||
bool isPureHealingSpell = isPureHealing(spell->mEffects);
|
||||
if (isPureHealingSpell && !allowHealing)
|
||||
continue;
|
||||
bestActionRating = rating;
|
||||
bestAction = std::make_unique<ActionSpell>(spell->mId);
|
||||
bestAction = std::make_unique<ActionSpell>(spell->mId, isPureHealingSpell);
|
||||
antiFleeRating = vanillaRateSpell(spell, actor, enemy);
|
||||
}
|
||||
}
|
||||
|
@ -263,9 +270,10 @@ namespace MWMechanics
|
|||
{
|
||||
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
|
||||
|
||||
bool pureHealing;
|
||||
for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it)
|
||||
{
|
||||
float rating = rateMagicItem(*it, actor, enemy);
|
||||
float rating = rateMagicItem(*it, actor, enemy, pureHealing);
|
||||
if (rating > bestActionRating)
|
||||
{
|
||||
bestActionRating = rating;
|
||||
|
|
|
@ -10,11 +10,18 @@ namespace MWMechanics
|
|||
{
|
||||
class Action
|
||||
{
|
||||
float mHealCooldown;
|
||||
|
||||
public:
|
||||
Action(bool healing)
|
||||
: mHealCooldown(healing ? 5.f : 0.f)
|
||||
{
|
||||
}
|
||||
virtual ~Action() {}
|
||||
virtual void prepare(const MWWorld::Ptr& actor) = 0;
|
||||
virtual float getCombatRange(bool& isRanged) const = 0;
|
||||
virtual float getActionCooldown() { return 0.f; }
|
||||
virtual float getActionCooldown() const { return 0.f; }
|
||||
virtual float getHealCooldown() const { return mHealCooldown; }
|
||||
virtual const ESM::Weapon* getWeapon() const { return nullptr; }
|
||||
virtual bool isAttackingOrSpell() const { return true; }
|
||||
virtual bool isFleeing() const { return false; }
|
||||
|
@ -23,10 +30,13 @@ namespace MWMechanics
|
|||
class ActionFlee : public Action
|
||||
{
|
||||
public:
|
||||
ActionFlee() {}
|
||||
ActionFlee()
|
||||
: Action(false)
|
||||
{
|
||||
}
|
||||
void prepare(const MWWorld::Ptr& actor) override {}
|
||||
float getCombatRange(bool& isRanged) const override { return 0.0f; }
|
||||
float getActionCooldown() override { return 3.0f; }
|
||||
float getActionCooldown() const override { return 3.0f; }
|
||||
bool isAttackingOrSpell() const override { return false; }
|
||||
bool isFleeing() const override { return true; }
|
||||
};
|
||||
|
@ -34,8 +44,9 @@ namespace MWMechanics
|
|||
class ActionSpell : public Action
|
||||
{
|
||||
public:
|
||||
ActionSpell(const ESM::RefId& spellId)
|
||||
: mSpellId(spellId)
|
||||
ActionSpell(const ESM::RefId& spellId, bool healing = false)
|
||||
: Action(healing)
|
||||
, mSpellId(spellId)
|
||||
{
|
||||
}
|
||||
ESM::RefId mSpellId;
|
||||
|
@ -48,8 +59,9 @@ namespace MWMechanics
|
|||
class ActionEnchantedItem : public Action
|
||||
{
|
||||
public:
|
||||
ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item)
|
||||
: mItem(item)
|
||||
ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item, bool healing)
|
||||
: Action(healing)
|
||||
, mItem(item)
|
||||
{
|
||||
}
|
||||
MWWorld::ContainerStoreIterator mItem;
|
||||
|
@ -58,14 +70,15 @@ namespace MWMechanics
|
|||
float getCombatRange(bool& isRanged) const override;
|
||||
|
||||
/// Since this action has no animation, apply a small cool down for using it
|
||||
float getActionCooldown() override { return 0.75f; }
|
||||
float getActionCooldown() const override { return 0.75f; }
|
||||
};
|
||||
|
||||
class ActionPotion : public Action
|
||||
{
|
||||
public:
|
||||
ActionPotion(const MWWorld::Ptr& potion)
|
||||
: mPotion(potion)
|
||||
ActionPotion(const MWWorld::Ptr& potion, bool healing)
|
||||
: Action(healing)
|
||||
, mPotion(potion)
|
||||
{
|
||||
}
|
||||
MWWorld::Ptr mPotion;
|
||||
|
@ -75,7 +88,7 @@ namespace MWMechanics
|
|||
bool isAttackingOrSpell() const override { return false; }
|
||||
|
||||
/// Since this action has no animation, apply a small cool down for using it
|
||||
float getActionCooldown() override { return 0.75f; }
|
||||
float getActionCooldown() const override { return 0.75f; }
|
||||
};
|
||||
|
||||
class ActionWeapon : public Action
|
||||
|
@ -87,7 +100,8 @@ namespace MWMechanics
|
|||
public:
|
||||
/// \a weapon may be empty for hand-to-hand combat
|
||||
ActionWeapon(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo = MWWorld::Ptr())
|
||||
: mAmmunition(ammo)
|
||||
: Action(false)
|
||||
, mAmmunition(ammo)
|
||||
, mWeapon(weapon)
|
||||
{
|
||||
}
|
||||
|
@ -97,7 +111,7 @@ namespace MWMechanics
|
|||
const ESM::Weapon* getWeapon() const override;
|
||||
};
|
||||
|
||||
std::unique_ptr<Action> prepareNextAction(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);
|
||||
std::unique_ptr<Action> prepareNextAction(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool allowHealing);
|
||||
float getBestActionRating(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);
|
||||
|
||||
float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool minusZDist = false);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include "spellpriority.hpp"
|
||||
#include "weaponpriority.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <components/esm3/loadench.hpp>
|
||||
#include <components/esm3/loadmgef.hpp>
|
||||
#include <components/esm3/loadrace.hpp>
|
||||
|
@ -86,6 +88,27 @@ namespace
|
|||
return spell.getCasterActorId() == actorId && spell.getId() == id;
|
||||
}) != active.end();
|
||||
}
|
||||
|
||||
float getRestoreMagickaPriority(const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
|
||||
{
|
||||
const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
|
||||
const MWMechanics::DynamicStat<float>& current = stats.getMagicka();
|
||||
const float magnitude = (effect.mMagnMin + effect.mMagnMax) / 2.f;
|
||||
const float toHeal = magnitude * std::max(1, effect.mDuration);
|
||||
float priority = 0.f;
|
||||
for (const ESM::Spell* spell : stats.getSpells())
|
||||
{
|
||||
auto found = std::find_if(spell->mEffects.mList.begin(), spell->mEffects.mList.end(),
|
||||
[&](const auto& e) { return &e == &effect; });
|
||||
if (found != spell->mEffects.mList.end()) // Prevent recursion
|
||||
continue;
|
||||
int cost = MWMechanics::calcSpellCost(*spell);
|
||||
if (cost < current.getCurrent() || cost > current.getCurrent() + toHeal)
|
||||
continue;
|
||||
priority = std::max(priority, MWMechanics::rateSpell(spell, actor, enemy, false));
|
||||
}
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
namespace MWMechanics
|
||||
|
@ -105,18 +128,19 @@ namespace MWMechanics
|
|||
return types;
|
||||
}
|
||||
|
||||
float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor)
|
||||
float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor, bool& pureHealing)
|
||||
{
|
||||
if (item.getType() != ESM::Potion::sRecordId)
|
||||
return 0.f;
|
||||
|
||||
const ESM::Potion* potion = item.get<ESM::Potion>()->mBase;
|
||||
pureHealing = isPureHealing(potion->mEffects);
|
||||
return rateEffects(potion->mEffects, actor, MWWorld::Ptr());
|
||||
}
|
||||
|
||||
float rateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy)
|
||||
float rateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool checkMagicka)
|
||||
{
|
||||
float successChance = MWMechanics::getSpellSuccessChance(spell, actor);
|
||||
float successChance = MWMechanics::getSpellSuccessChance(spell, actor, nullptr, true, checkMagicka);
|
||||
if (successChance == 0.f)
|
||||
return 0.f;
|
||||
|
||||
|
@ -136,19 +160,21 @@ namespace MWMechanics
|
|||
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))
|
||||
if (((types & Touch) || (types & Target)) && !enemy.isEmpty() && 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)
|
||||
float rateMagicItem(
|
||||
const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool& pureHealing)
|
||||
{
|
||||
if (ptr.getClass().getEnchantment(ptr).empty())
|
||||
return 0.f;
|
||||
|
||||
const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get<ESM::Enchantment>().find(
|
||||
ptr.getClass().getEnchantment(ptr));
|
||||
pureHealing = isPureHealing(enchantment->mEffects);
|
||||
|
||||
// Spells don't stack, so early out if the spell is still active on the target
|
||||
int types = getRangeTypes(enchantment->mEffects);
|
||||
|
@ -405,30 +431,47 @@ namespace MWMechanics
|
|||
return 0.f;
|
||||
break;
|
||||
|
||||
case ESM::MagicEffect::AbsorbMagicka:
|
||||
if (!enemy.isEmpty() && enemy.getClass().getCreatureStats(enemy).getMagicka().getCurrent() <= 0.f)
|
||||
{
|
||||
rating = 0.5f;
|
||||
float priority = getRestoreMagickaPriority(effect, actor, enemy);
|
||||
if (priority == 0.f)
|
||||
priority = -1.f;
|
||||
rating *= priority;
|
||||
}
|
||||
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)
|
||||
const float damage = current.getModified() - current.getCurrent();
|
||||
float priority = 0.f;
|
||||
if (effect.mEffectID == ESM::MagicEffect::RestoreHealth)
|
||||
priority = 4.f;
|
||||
else if (effect.mEffectID == ESM::MagicEffect::RestoreMagicka)
|
||||
priority = std::max(0.1f, getRestoreMagickaPriority(effect, actor, enemy));
|
||||
else if (effect.mEffectID == ESM::MagicEffect::RestoreFatigue)
|
||||
priority = 2.f;
|
||||
float overheal = 0.f;
|
||||
float heal = toHeal;
|
||||
if (damage < toHeal && current.getCurrent() > current.getModified() * 0.5)
|
||||
{
|
||||
return 10000.f * priority
|
||||
- (toHeal
|
||||
- (current.getModified() - current.getCurrent())); // prefer the most fitting potion
|
||||
overheal = toHeal - damage;
|
||||
heal = damage;
|
||||
}
|
||||
else
|
||||
return -10000.f * priority; // Save for later
|
||||
|
||||
priority = (std::pow(priority + heal / current.getModified(), damage * 4.f / current.getModified())
|
||||
- priority - overheal / current.getModified())
|
||||
/ priority;
|
||||
rating = priority;
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
@ -25,9 +25,11 @@ namespace MWMechanics
|
|||
|
||||
int getRangeTypes(const ESM::EffectList& effects);
|
||||
|
||||
float rateSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);
|
||||
float rateMagicItem(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);
|
||||
float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor);
|
||||
float rateSpell(
|
||||
const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool checkMagicka = true);
|
||||
float rateMagicItem(
|
||||
const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, bool& pureHealing);
|
||||
float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor, bool& pureHealing);
|
||||
|
||||
/// @note target may be empty
|
||||
float rateEffect(const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);
|
||||
|
|
|
@ -257,4 +257,13 @@ namespace MWMechanics
|
|||
const auto spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(spellId);
|
||||
return spell && spellIncreasesSkill(spell);
|
||||
}
|
||||
|
||||
bool isPureHealing(const ESM::EffectList& list)
|
||||
{
|
||||
auto nonHealing = std::find_if(list.mList.begin(), list.mList.end(), [](const auto& effect) {
|
||||
return effect.mEffectID < ESM::MagicEffect::RestoreAttribute
|
||||
|| effect.mEffectID > ESM::MagicEffect::RestoreSkill;
|
||||
});
|
||||
return nonHealing == list.mList.end();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
namespace ESM
|
||||
{
|
||||
struct EffectList;
|
||||
struct ENAMstruct;
|
||||
struct Enchantment;
|
||||
struct MagicEffect;
|
||||
|
@ -54,6 +55,8 @@ namespace MWMechanics
|
|||
/// Get whether or not the given spell contributes to skill progress.
|
||||
bool spellIncreasesSkill(const ESM::Spell* spell);
|
||||
bool spellIncreasesSkill(const ESM::RefId& spellId);
|
||||
|
||||
bool isPureHealing(const ESM::EffectList& list);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Reference in a new issue