mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-26 18:35:20 +00:00
Merge remote-tracking branch 'scrawl/ai'
This commit is contained in:
commit
9197d4b653
@ -77,7 +77,7 @@ add_openmw_dir (mwmechanics
|
|||||||
mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects
|
mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects
|
||||||
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor
|
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor
|
||||||
aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting
|
aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting
|
||||||
disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling
|
disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction
|
||||||
)
|
)
|
||||||
|
|
||||||
add_openmw_dir (mwstate
|
add_openmw_dir (mwstate
|
||||||
|
@ -248,6 +248,12 @@ namespace MWMechanics
|
|||||||
|
|
||||||
if (caster.getRefData().getHandle() == "player")
|
if (caster.getRefData().getHandle() == "player")
|
||||||
MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}");
|
MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}");
|
||||||
|
|
||||||
|
const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>()
|
||||||
|
.search("VFX_Soul_Trap");
|
||||||
|
if (fx)
|
||||||
|
MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel,
|
||||||
|
"", Ogre::Vector3(mCreature.getRefData().getPosition().pos));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
#include "movement.hpp"
|
#include "movement.hpp"
|
||||||
#include "character.hpp" // fixme: for getActiveWeapon
|
#include "character.hpp" // fixme: for getActiveWeapon
|
||||||
|
|
||||||
|
#include "aicombataction.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static float sgn(Ogre::Radian a)
|
static float sgn(Ogre::Radian a)
|
||||||
@ -107,6 +109,7 @@ namespace MWMechanics
|
|||||||
|
|
||||||
void AiCombat::init()
|
void AiCombat::init()
|
||||||
{
|
{
|
||||||
|
mActionCooldown = 0;
|
||||||
mTimerAttack = 0;
|
mTimerAttack = 0;
|
||||||
mTimerReact = 0;
|
mTimerReact = 0;
|
||||||
mTimerCombatMove = 0;
|
mTimerCombatMove = 0;
|
||||||
@ -246,6 +249,8 @@ namespace MWMechanics
|
|||||||
|
|
||||||
actorClass.getCreatureStats(actor).setAttackingOrSpell(mAttack);
|
actorClass.getCreatureStats(actor).setAttackingOrSpell(mAttack);
|
||||||
|
|
||||||
|
mActionCooldown -= duration;
|
||||||
|
|
||||||
float tReaction = 0.25f;
|
float tReaction = 0.25f;
|
||||||
if(mTimerReact < tReaction)
|
if(mTimerReact < tReaction)
|
||||||
{
|
{
|
||||||
@ -263,19 +268,33 @@ namespace MWMechanics
|
|||||||
mCell = actor.getCell();
|
mCell = actor.getCell();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ESM::Weapon *weapon = NULL;
|
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
|
||||||
MWMechanics::WeaponType weaptype;
|
if (!anim) // shouldn't happen
|
||||||
float weapRange = 1.0f;
|
return false;
|
||||||
|
|
||||||
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
|
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
|
||||||
|
|
||||||
|
if (mActionCooldown > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float rangeAttack = 0;
|
||||||
|
float rangeFollow = 0;
|
||||||
|
if (anim->upperBodyReady())
|
||||||
|
{
|
||||||
|
mCurrentAction = prepareNextAction(actor, target);
|
||||||
|
mActionCooldown = mCurrentAction->getActionCooldown();
|
||||||
|
}
|
||||||
|
if (mCurrentAction.get())
|
||||||
|
mCurrentAction->getCombatRange(rangeAttack, rangeFollow);
|
||||||
|
|
||||||
|
// FIXME: consider moving this stuff to ActionWeapon::getCombatRange
|
||||||
|
const ESM::Weapon *weapon = NULL;
|
||||||
|
MWMechanics::WeaponType weaptype = WeapType_None;
|
||||||
|
float weapRange = 1.0f;
|
||||||
|
|
||||||
// Get weapon characteristics
|
// Get weapon characteristics
|
||||||
if (actorClass.hasInventoryStore(actor))
|
if (actorClass.hasInventoryStore(actor))
|
||||||
{
|
{
|
||||||
MWMechanics::DrawState_ state = actorClass.getCreatureStats(actor).getDrawState();
|
|
||||||
if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing)
|
|
||||||
actorClass.getCreatureStats(actor).setDrawState(MWMechanics::DrawState_Weapon);
|
|
||||||
|
|
||||||
// TODO: Check equipped weapon and equip a different one if we can't attack with it
|
// TODO: Check equipped weapon and equip a different one if we can't attack with it
|
||||||
// (e.g. no ammunition, or wrong type of ammunition equipped, etc. autoEquip is not very smart in this regard))
|
// (e.g. no ammunition, or wrong type of ammunition equipped, etc. autoEquip is not very smart in this regard))
|
||||||
|
|
||||||
@ -285,11 +304,11 @@ namespace MWMechanics
|
|||||||
|
|
||||||
if (weaptype == WeapType_HandToHand)
|
if (weaptype == WeapType_HandToHand)
|
||||||
{
|
{
|
||||||
static float fHandToHandReach =
|
static float fHandToHandReach =
|
||||||
world->getStore().get<ESM::GameSetting>().find("fHandToHandReach")->getFloat();
|
world->getStore().get<ESM::GameSetting>().find("fHandToHandReach")->getFloat();
|
||||||
weapRange = fHandToHandReach;
|
weapRange = fHandToHandReach;
|
||||||
}
|
}
|
||||||
else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell)
|
else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell && weaptype != WeapType_None)
|
||||||
{
|
{
|
||||||
// All other WeapTypes are actually weapons, so get<ESM::Weapon> is safe.
|
// All other WeapTypes are actually weapons, so get<ESM::Weapon> is safe.
|
||||||
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
|
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
|
||||||
@ -303,19 +322,26 @@ namespace MWMechanics
|
|||||||
weapRange = 150.0f; //TODO: use true attack range (the same problem in Creature::hit)
|
weapRange = 150.0f; //TODO: use true attack range (the same problem in Creature::hit)
|
||||||
}
|
}
|
||||||
|
|
||||||
float rangeAttack;
|
|
||||||
float rangeFollow;
|
|
||||||
bool distantCombat = false;
|
bool distantCombat = false;
|
||||||
if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown)
|
if (weaptype != WeapType_Spell)
|
||||||
{
|
{
|
||||||
rangeAttack = 1000; // TODO: should depend on archer skill
|
// TODO: move to ActionWeapon
|
||||||
rangeFollow = 0; // not needed in ranged combat
|
if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown)
|
||||||
distantCombat = true;
|
{
|
||||||
|
rangeAttack = 1000;
|
||||||
|
rangeFollow = 0; // not needed in ranged combat
|
||||||
|
distantCombat = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rangeAttack = weapRange;
|
||||||
|
rangeFollow = 300;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rangeAttack = weapRange;
|
distantCombat = (rangeAttack > 500);
|
||||||
rangeFollow = 300;
|
weapRange = 150.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start new attack
|
// start new attack
|
||||||
@ -345,7 +371,7 @@ namespace MWMechanics
|
|||||||
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
|
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -750,7 +776,9 @@ void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations
|
|||||||
MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype);
|
MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype);
|
||||||
|
|
||||||
float weapSpeed;
|
float weapSpeed;
|
||||||
if (weaptype != MWMechanics::WeapType_HandToHand)
|
if (weaptype != MWMechanics::WeapType_HandToHand
|
||||||
|
&& weaptype != MWMechanics::WeapType_Spell
|
||||||
|
&& weaptype != MWMechanics::WeapType_None)
|
||||||
{
|
{
|
||||||
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
|
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
|
||||||
weapSpeed = weapon->mData.mSpeed;
|
weapSpeed = weapon->mData.mSpeed;
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
|
|
||||||
|
#include <boost/shared_ptr.hpp>
|
||||||
|
|
||||||
namespace ESM
|
namespace ESM
|
||||||
{
|
{
|
||||||
namespace AiSequence
|
namespace AiSequence
|
||||||
@ -24,6 +26,8 @@ namespace ESM
|
|||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
|
class Action;
|
||||||
|
|
||||||
/// \brief Causes the actor to fight another actor
|
/// \brief Causes the actor to fight another actor
|
||||||
class AiCombat : public AiPackage
|
class AiCombat : public AiPackage
|
||||||
{
|
{
|
||||||
@ -79,6 +83,9 @@ namespace MWMechanics
|
|||||||
const MWWorld::CellStore* mCell;
|
const MWWorld::CellStore* mCell;
|
||||||
ObstacleCheck mObstacleCheck;
|
ObstacleCheck mObstacleCheck;
|
||||||
|
|
||||||
|
boost::shared_ptr<Action> mCurrentAction;
|
||||||
|
float mActionCooldown;
|
||||||
|
|
||||||
void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
463
apps/openmw/mwmechanics/aicombataction.cpp
Normal file
463
apps/openmw/mwmechanics/aicombataction.cpp
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
#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/creaturestats.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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& target, 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, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
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& target)
|
||||||
|
{
|
||||||
|
const CreatureStats& stats = actor.getClass().getCreatureStats(actor);
|
||||||
|
|
||||||
|
// Never casting racial spells (ST_Power and F_Always)
|
||||||
|
if (spell->mData.mType != ESM::Spell::ST_Spell || spell->mData.mFlags & ESM::Spell::F_Always)
|
||||||
|
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)) && target.getClass().getCreatureStats(target).getActiveSpells().isSpellActive(spell->mId))
|
||||||
|
return 0.f;
|
||||||
|
|
||||||
|
return rateEffects(spell->mEffects, actor, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
float rateMagicItem(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor, const MWWorld::Ptr& target)
|
||||||
|
{
|
||||||
|
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, target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rateEffect(const ESM::ENAMstruct &effect, const MWWorld::Ptr &actor, const MWWorld::Ptr &target)
|
||||||
|
{
|
||||||
|
// NOTE: target may be empty
|
||||||
|
|
||||||
|
float baseRating = 1;
|
||||||
|
switch (effect.mEffectID)
|
||||||
|
{
|
||||||
|
case ESM::MagicEffect::Soultrap:
|
||||||
|
case ESM::MagicEffect::AlmsiviIntervention:
|
||||||
|
case ESM::MagicEffect::DivineIntervention:
|
||||||
|
case ESM::MagicEffect::CalmCreature:
|
||||||
|
case ESM::MagicEffect::CalmHumanoid:
|
||||||
|
case ESM::MagicEffect::Charm:
|
||||||
|
case ESM::MagicEffect::DetectAnimal:
|
||||||
|
case ESM::MagicEffect::DetectEnchantment:
|
||||||
|
case ESM::MagicEffect::DetectKey:
|
||||||
|
case ESM::MagicEffect::FrenzyCreature:
|
||||||
|
case ESM::MagicEffect::FrenzyHumanoid:
|
||||||
|
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:
|
||||||
|
return 0.f;
|
||||||
|
case ESM::MagicEffect::Feather:
|
||||||
|
return 0.f; // TODO: check if target is overencumbered
|
||||||
|
case ESM::MagicEffect::Levitate:
|
||||||
|
return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway
|
||||||
|
// TODO: check if Beast race (can't wear boots or helm)
|
||||||
|
/*
|
||||||
|
case ESM::MagicEffect::BoundBoots:
|
||||||
|
case ESM::MagicEffect::BoundHelm:
|
||||||
|
*/
|
||||||
|
case ESM::MagicEffect::RestoreHealth:
|
||||||
|
case ESM::MagicEffect::RestoreMagicka:
|
||||||
|
case ESM::MagicEffect::RestoreFatigue:
|
||||||
|
if (effect.mRange == ESM::RT_Self)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
else
|
||||||
|
return -10000.f; // Save for later
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Give a small boost to all direct damage effects. This is combat, after all!
|
||||||
|
case ESM::MagicEffect::FireDamage:
|
||||||
|
case ESM::MagicEffect::ShockDamage:
|
||||||
|
case ESM::MagicEffect::FrostDamage:
|
||||||
|
case ESM::MagicEffect::Poison:
|
||||||
|
case ESM::MagicEffect::AbsorbHealth:
|
||||||
|
case ESM::MagicEffect::DamageHealth:
|
||||||
|
baseRating *= 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESM::MagicEffect::Paralyze: // *Evil laughter*
|
||||||
|
baseRating *= 5;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// TODO: rate these effects very high if we are currently suffering from negative effects that could be cured
|
||||||
|
case ESM::MagicEffect::Dispel:
|
||||||
|
case ESM::MagicEffect::CureParalyzation:
|
||||||
|
case ESM::MagicEffect::CurePoison:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: for non-cumulative effects (e.g. paralyze), check if the target is already suffering from them
|
||||||
|
|
||||||
|
// TODO: could take into account target's resistance/weakness against the effect
|
||||||
|
|
||||||
|
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effect.mEffectID);
|
||||||
|
|
||||||
|
baseRating *= magicEffect->mData.mBaseCost;
|
||||||
|
|
||||||
|
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
|
||||||
|
return 0.f; // No clue how useful this would be; will need special cases for each effect
|
||||||
|
|
||||||
|
float rating = baseRating * (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& target)
|
||||||
|
{
|
||||||
|
// NOTE: target 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, target);
|
||||||
|
}
|
||||||
|
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 &target)
|
||||||
|
{
|
||||||
|
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().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, target);
|
||||||
|
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, target, 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, target, 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, target, -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 = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(it->first);
|
||||||
|
|
||||||
|
float rating = rateSpell(spell, actor, target);
|
||||||
|
if (rating > bestActionRating)
|
||||||
|
{
|
||||||
|
bestActionRating = rating;
|
||||||
|
bestAction.reset(new ActionSpell(spell->mId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestAction.get())
|
||||||
|
bestAction->prepare(actor);
|
||||||
|
|
||||||
|
return bestAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
89
apps/openmw/mwmechanics/aicombataction.hpp
Normal file
89
apps/openmw/mwmechanics/aicombataction.hpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#ifndef OPENMW_AICOMBAT_ACTION_H
|
||||||
|
#define OPENMW_AICOMBAT_ACTION_H
|
||||||
|
|
||||||
|
#include <boost/shared_ptr.hpp>
|
||||||
|
|
||||||
|
#include "../mwworld/ptr.hpp"
|
||||||
|
#include "../mwworld/containerstore.hpp"
|
||||||
|
|
||||||
|
#include <components/esm/loadspel.hpp>
|
||||||
|
|
||||||
|
namespace MWMechanics
|
||||||
|
{
|
||||||
|
|
||||||
|
class Action
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void prepare(const MWWorld::Ptr& actor) = 0;
|
||||||
|
virtual void getCombatRange (float& rangeAttack, float& rangeFollow) = 0;
|
||||||
|
virtual float getActionCooldown() { return 0.f; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActionSpell : public Action
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ActionSpell(const std::string& spellId) : mSpellId(spellId) {}
|
||||||
|
std::string mSpellId;
|
||||||
|
/// Sets the given spell as selected on the actor's spell list.
|
||||||
|
virtual void prepare(const MWWorld::Ptr& actor);
|
||||||
|
|
||||||
|
virtual void getCombatRange (float& rangeAttack, float& rangeFollow);
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActionEnchantedItem : public Action
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item) : mItem(item) {}
|
||||||
|
MWWorld::ContainerStoreIterator mItem;
|
||||||
|
/// Sets the given item as selected enchanted item in the actor's InventoryStore.
|
||||||
|
virtual void prepare(const MWWorld::Ptr& actor);
|
||||||
|
virtual void getCombatRange (float& rangeAttack, float& rangeFollow);
|
||||||
|
|
||||||
|
/// Since this action has no animation, apply a small cool down for using it
|
||||||
|
virtual float getActionCooldown() { return 1.f; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActionPotion : public Action
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ActionPotion(const MWWorld::Ptr& potion) : mPotion(potion) {}
|
||||||
|
MWWorld::Ptr mPotion;
|
||||||
|
/// Drinks the given potion.
|
||||||
|
virtual void prepare(const MWWorld::Ptr& actor);
|
||||||
|
virtual void getCombatRange (float& rangeAttack, float& rangeFollow);
|
||||||
|
|
||||||
|
/// Since this action has no animation, apply a small cool down for using it
|
||||||
|
virtual float getActionCooldown() { return 1.f; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActionWeapon : public Action
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
MWWorld::Ptr mAmmunition;
|
||||||
|
MWWorld::Ptr mWeapon;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// \a weapon may be empty for hand-to-hand combat
|
||||||
|
ActionWeapon(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo = MWWorld::Ptr())
|
||||||
|
: mWeapon(weapon), mAmmunition(ammo) {}
|
||||||
|
/// Equips the given weapon.
|
||||||
|
virtual void prepare(const MWWorld::Ptr& actor);
|
||||||
|
virtual void getCombatRange (float& rangeAttack, float& rangeFollow);
|
||||||
|
};
|
||||||
|
|
||||||
|
float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
||||||
|
float rateMagicItem (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr &target);
|
||||||
|
float ratePotion (const MWWorld::Ptr& item, const MWWorld::Ptr &actor);
|
||||||
|
/// @param type Skip all weapons that are not of this type (i.e. return rating 0)
|
||||||
|
float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target,
|
||||||
|
int type=-1, float arrowRating=0.f, float boltRating=0.f);
|
||||||
|
|
||||||
|
/// @note target may be empty
|
||||||
|
float rateEffect (const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
||||||
|
/// @note target may be empty
|
||||||
|
float rateEffects (const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
||||||
|
|
||||||
|
boost::shared_ptr<Action> prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -1198,7 +1198,7 @@ void Animation::detachObjectFromBone(Ogre::MovableObject *obj)
|
|||||||
mSkelBase->detachObjectFromBone(obj);
|
mSkelBase->detachObjectFromBone(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Animation::allowSwitchViewMode() const
|
bool Animation::upperBodyReady() const
|
||||||
{
|
{
|
||||||
for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter)
|
for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter)
|
||||||
{
|
{
|
||||||
|
@ -263,8 +263,8 @@ public:
|
|||||||
/** Returns true if the named animation group is playing. */
|
/** Returns true if the named animation group is playing. */
|
||||||
bool isPlaying(const std::string &groupname) const;
|
bool isPlaying(const std::string &groupname) const;
|
||||||
|
|
||||||
//Checks if playing any animation which shouldn't be stopped when switching camera view modes
|
/// Returns true if no important animations are currently playing on the upper body.
|
||||||
bool allowSwitchViewMode() const;
|
bool upperBodyReady() const;
|
||||||
|
|
||||||
/** Gets info about the given animation group.
|
/** Gets info about the given animation group.
|
||||||
* \param groupname Animation group to check.
|
* \param groupname Animation group to check.
|
||||||
|
@ -107,7 +107,7 @@ namespace MWRender
|
|||||||
|
|
||||||
void Camera::update(float duration, bool paused)
|
void Camera::update(float duration, bool paused)
|
||||||
{
|
{
|
||||||
if (mAnimation->allowSwitchViewMode())
|
if (mAnimation->upperBodyReady())
|
||||||
{
|
{
|
||||||
// Now process the view changes we queued earlier
|
// Now process the view changes we queued earlier
|
||||||
if (mVanityToggleQueued)
|
if (mVanityToggleQueued)
|
||||||
@ -144,7 +144,7 @@ namespace MWRender
|
|||||||
{
|
{
|
||||||
// Changing the view will stop all playing animations, so if we are playing
|
// Changing the view will stop all playing animations, so if we are playing
|
||||||
// anything important, queue the view change for later
|
// anything important, queue the view change for later
|
||||||
if (!mAnimation->allowSwitchViewMode() && !force)
|
if (!mAnimation->upperBodyReady() && !force)
|
||||||
{
|
{
|
||||||
mViewModeToggleQueued = true;
|
mViewModeToggleQueued = true;
|
||||||
return;
|
return;
|
||||||
@ -207,7 +207,7 @@ namespace MWRender
|
|||||||
|
|
||||||
void Camera::togglePreviewMode(bool enable)
|
void Camera::togglePreviewMode(bool enable)
|
||||||
{
|
{
|
||||||
if (mFirstPersonView && !mAnimation->allowSwitchViewMode())
|
if (mFirstPersonView && !mAnimation->upperBodyReady())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(mPreviewMode == enable)
|
if(mPreviewMode == enable)
|
||||||
|
@ -198,11 +198,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't auto-equip probes or lockpicks. NPCs can't use them (yet). And AiCombat would attempt to "attack" with them.
|
|
||||||
// NOTE: In the future AiCombat should handle equipping appropriate weapons
|
|
||||||
if (test.getTypeName() == typeid(ESM::Lockpick).name() || test.getTypeName() == typeid(ESM::Probe).name())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Only autoEquip if we are the original owner of the item.
|
// Only autoEquip if we are the original owner of the item.
|
||||||
// This stops merchants from auto equipping anything you sell to them.
|
// This stops merchants from auto equipping anything you sell to them.
|
||||||
// ...unless this is a companion, he should always equip items given to him.
|
// ...unless this is a companion, he should always equip items given to him.
|
||||||
@ -219,6 +214,10 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor)
|
|||||||
for (std::vector<int>::const_iterator iter2 (itemsSlots.first.begin());
|
for (std::vector<int>::const_iterator iter2 (itemsSlots.first.begin());
|
||||||
iter2!=itemsSlots.first.end(); ++iter2)
|
iter2!=itemsSlots.first.end(); ++iter2)
|
||||||
{
|
{
|
||||||
|
if (*iter2 == Slot_CarriedRight) // Items in right hand are situational use, so don't equip them.
|
||||||
|
// Equipping weapons is handled by AiCombat. Anything else (lockpicks, probes) can't be used by NPCs anyway (yet)
|
||||||
|
continue;
|
||||||
|
|
||||||
bool use = false;
|
bool use = false;
|
||||||
|
|
||||||
if (slots_.at (*iter2)==end())
|
if (slots_.at (*iter2)==end())
|
||||||
|
@ -141,7 +141,7 @@ namespace MWWorld
|
|||||||
/// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item.
|
/// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item.
|
||||||
|
|
||||||
void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor);
|
void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor);
|
||||||
///< \note \a iterator can be an end-iterator
|
///< \warning \a iterator can not be an end()-iterator, use unequip function instead
|
||||||
|
|
||||||
void setSelectedEnchantItem(const ContainerStoreIterator& iterator);
|
void setSelectedEnchantItem(const ContainerStoreIterator& iterator);
|
||||||
///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used")
|
///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user