From 0fe9612afbe9adcd1f8f14f0cd7da93cfd1b8eaa Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 00:41:52 +0200 Subject: [PATCH 1/7] Implement basic spellcasting AI (Fixes #961) Select a weapon to attack with in AiCombat and equip it (Fixes #1609, Fixes #1772) --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwmechanics/aicombat.cpp | 61 +++- apps/openmw/mwmechanics/aicombat.hpp | 7 + apps/openmw/mwmechanics/aicombataction.cpp | 397 +++++++++++++++++++++ apps/openmw/mwmechanics/aicombataction.hpp | 83 +++++ apps/openmw/mwrender/animation.cpp | 2 +- apps/openmw/mwrender/animation.hpp | 4 +- apps/openmw/mwrender/camera.cpp | 6 +- apps/openmw/mwworld/inventorystore.hpp | 2 +- 9 files changed, 539 insertions(+), 25 deletions(-) create mode 100644 apps/openmw/mwmechanics/aicombataction.cpp create mode 100644 apps/openmw/mwmechanics/aicombataction.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 6df6210ee..a7e263b11 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -77,7 +77,7 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor 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 diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index d59b0a3ec..94f5574bd 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -22,6 +22,8 @@ #include "movement.hpp" #include "character.hpp" // fixme: for getActiveWeapon +#include "aicombataction.hpp" + namespace { static float sgn(Ogre::Radian a) @@ -107,6 +109,7 @@ namespace MWMechanics void AiCombat::init() { + mActionCooldown = 0; mTimerAttack = 0; mTimerReact = 0; mTimerCombatMove = 0; @@ -246,6 +249,8 @@ namespace MWMechanics actorClass.getCreatureStats(actor).setAttackingOrSpell(mAttack); + mActionCooldown -= duration; + float tReaction = 0.25f; if(mTimerReact < tReaction) { @@ -263,19 +268,33 @@ namespace MWMechanics mCell = actor.getCell(); } + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor); + if (!anim) // shouldn't happen + return false; + + 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; float weapRange = 1.0f; - actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); - // Get weapon characteristics 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 // (e.g. no ammunition, or wrong type of ammunition equipped, etc. autoEquip is not very smart in this regard)) @@ -285,7 +304,7 @@ namespace MWMechanics if (weaptype == WeapType_HandToHand) { - static float fHandToHandReach = + static float fHandToHandReach = world->getStore().get().find("fHandToHandReach")->getFloat(); weapRange = fHandToHandReach; } @@ -303,19 +322,26 @@ namespace MWMechanics weapRange = 150.0f; //TODO: use true attack range (the same problem in Creature::hit) } - float rangeAttack; - float rangeFollow; 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 - rangeFollow = 0; // not needed in ranged combat - distantCombat = true; + // TODO: move to ActionWeapon + if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) + { + rangeAttack = 1000; + rangeFollow = 0; // not needed in ranged combat + distantCombat = true; + } + else + { + rangeAttack = weapRange; + rangeFollow = 300; + } } else { - rangeAttack = weapRange; - rangeFollow = 300; + distantCombat = (rangeAttack > 500); + weapRange = 150.f; } // start new attack @@ -345,7 +371,7 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } } - } + } } @@ -750,7 +776,8 @@ void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype); float weapSpeed; - if (weaptype != MWMechanics::WeapType_HandToHand) + if (weaptype != MWMechanics::WeapType_HandToHand + && weaptype != MWMechanics::WeapType_Spell) { weapon = weaponSlot->get()->mBase; weapSpeed = weapon->mData.mSpeed; diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 311dee617..916a1a1d5 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -14,6 +14,8 @@ #include "../mwbase/world.hpp" +#include + namespace ESM { namespace AiSequence @@ -24,6 +26,8 @@ namespace ESM namespace MWMechanics { + class Action; + /// \brief Causes the actor to fight another actor class AiCombat : public AiPackage { @@ -79,6 +83,9 @@ namespace MWMechanics const MWWorld::CellStore* mCell; ObstacleCheck mObstacleCheck; + boost::shared_ptr mCurrentAction; + float mActionCooldown; + void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); }; } diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp new file mode 100644 index 000000000..7a8ee75fc --- /dev/null +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -0,0 +1,397 @@ +#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 +#include + +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::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()->mBase; + return rateEffects(potion->mEffects, actor, MWWorld::Ptr()); + } + + float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + { + if (item.getTypeName() != typeid(ESM::Weapon).name()) + return 0.f; + + const ESM::Weapon* weapon = item.get()->mBase; + + // TODO: Check that we have ammunition if needed + + 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)) + rating *= item.getClass().getItemHealth(item) / float(item.getClass().getItemMaxHealth(item)); + + rating *= actor.getClass().getSkill(actor, item.getClass().getEquipmentSkill(item)) / 100.f; + + if (!weapon->mEnchant.empty()) + { + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().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); + } + 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().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& 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().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::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().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().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); + } + // TODO: equip ammunition and shield where needed + } + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); + } + + void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) + { + // Already done in AiCombat itself + } + + boost::shared_ptr 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 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)); + } + } + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateWeapon(*it, actor, target); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionWeapon(*it)); + } + } + } + + for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().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; + } + +} diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp new file mode 100644 index 000000000..8c95e14c6 --- /dev/null +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -0,0 +1,83 @@ +#ifndef OPENMW_AICOMBAT_ACTION_H +#define OPENMW_AICOMBAT_ACTION_H + +#include + +#include "../mwworld/ptr.hpp" +#include "../mwworld/containerstore.hpp" + +#include + +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 + { + public: + /// \a weapon may be empty for hand-to-hand combat + ActionWeapon(const MWWorld::Ptr& weapon) : mWeapon(weapon) {} + MWWorld::Ptr mWeapon; + /// 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); + float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor); + + /// @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 prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& target); +} + +#endif diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index ab181ca77..a8b453e89 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1198,7 +1198,7 @@ void Animation::detachObjectFromBone(Ogre::MovableObject *obj) mSkelBase->detachObjectFromBone(obj); } -bool Animation::allowSwitchViewMode() const +bool Animation::upperBodyReady() const { for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter) { diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index b85e74549..4f53a737b 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -263,8 +263,8 @@ public: /** Returns true if the named animation group is playing. */ bool isPlaying(const std::string &groupname) const; - //Checks if playing any animation which shouldn't be stopped when switching camera view modes - bool allowSwitchViewMode() const; + /// Returns true if no important animations are currently playing on the upper body. + bool upperBodyReady() const; /** Gets info about the given animation group. * \param groupname Animation group to check. diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 4580bae70..f38e00cce 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -107,7 +107,7 @@ namespace MWRender void Camera::update(float duration, bool paused) { - if (mAnimation->allowSwitchViewMode()) + if (mAnimation->upperBodyReady()) { // Now process the view changes we queued earlier if (mVanityToggleQueued) @@ -144,7 +144,7 @@ namespace MWRender { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later - if (!mAnimation->allowSwitchViewMode() && !force) + if (!mAnimation->upperBodyReady() && !force) { mViewModeToggleQueued = true; return; @@ -207,7 +207,7 @@ namespace MWRender void Camera::togglePreviewMode(bool enable) { - if (mFirstPersonView && !mAnimation->allowSwitchViewMode()) + if (mFirstPersonView && !mAnimation->upperBodyReady()) return; if(mPreviewMode == enable) diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 95b956907..41caae4e5 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -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. 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); ///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used") From f1d72419de328fa734ff54dabdeef093f85f6ce7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 00:59:54 +0200 Subject: [PATCH 2/7] Don't equip weapons in autoEquip, AiCombat does that now (Fixes #1451) --- apps/openmw/mwworld/inventorystore.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index dcbe707b7..7ea4a0bf1 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -198,11 +198,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) 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. // This stops merchants from auto equipping anything you sell to them. // ...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::const_iterator iter2 (itemsSlots.first.begin()); 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; if (slots_.at (*iter2)==end()) From 0bdc1b243ae23e8f51a6cced0a5016ed02f66abf Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 01:54:32 +0200 Subject: [PATCH 3/7] Consider weapon ammunition in combat AI (Fixes #1576) --- apps/openmw/mwmechanics/aicombataction.cpp | 74 ++++++++++++++++++++-- apps/openmw/mwmechanics/aicombataction.hpp | 12 +++- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 7a8ee75fc..528bd6d6c 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -73,14 +73,16 @@ namespace MWMechanics return rateEffects(potion->mEffects, actor, MWWorld::Ptr()); } - float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + 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()->mBase; - // TODO: Check that we have ammunition if needed + if (type != -1 && weapon->mData.mType != type) + return 0.f; float rating=0.f; @@ -102,7 +104,20 @@ namespace MWMechanics if (item.getClass().hasItemHealth(item)) rating *= item.getClass().getItemHealth(item) / float(item.getClass().getItemMaxHealth(item)); - rating *= actor.getClass().getSkill(actor, item.getClass().getEquipmentSkill(item)) / 100.f; + 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()) { @@ -112,6 +127,11 @@ namespace MWMechanics || 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; } @@ -323,7 +343,12 @@ namespace MWMechanics MWWorld::ActionEquip equip(mWeapon); equip.execute(actor); } - // TODO: equip ammunition and shield where needed + + if (!mAmmunition.isEmpty()) + { + MWWorld::ActionEquip equip(mAmmunition); + equip.execute(actor); + } } actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); } @@ -365,13 +390,50 @@ namespace MWMechanics } } + 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) { - float rating = rateWeapon(*it, actor, target); + std::vector 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()->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)); + bestAction.reset(new ActionWeapon(*it, ammo)); } } } diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index 8c95e14c6..487405838 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -58,10 +58,14 @@ namespace MWMechanics 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) : mWeapon(weapon) {} - MWWorld::Ptr mWeapon; + 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); @@ -70,7 +74,9 @@ namespace MWMechanics 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); - float rateWeapon (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); From 2e623bac5a6c778f947ec4c66db8cb56c9f06f06 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 02:01:41 +0200 Subject: [PATCH 4/7] Don't attempt to use broken weapons in AI --- apps/openmw/mwmechanics/aicombataction.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 528bd6d6c..c10c982b8 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -102,7 +102,11 @@ namespace MWMechanics } 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) { From 1a98f8ca93cb56c81dedcafa4fbd3d4e39e1768f Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 02:09:00 +0200 Subject: [PATCH 5/7] Invalid iterator fix --- apps/openmw/mwmechanics/aicombat.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 94f5574bd..a51049e85 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -289,7 +289,7 @@ namespace MWMechanics // FIXME: consider moving this stuff to ActionWeapon::getCombatRange const ESM::Weapon *weapon = NULL; - MWMechanics::WeaponType weaptype; + MWMechanics::WeaponType weaptype = WeapType_None; float weapRange = 1.0f; // Get weapon characteristics @@ -308,7 +308,7 @@ namespace MWMechanics world->getStore().get().find("fHandToHandReach")->getFloat(); 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 is safe. weapon = weaponSlot->get()->mBase; From f8f4d2dfdb37b01f33624ca48cdb0c23744bdbf7 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 02:14:30 +0200 Subject: [PATCH 6/7] Another invalid iterator fix --- apps/openmw/mwmechanics/aicombat.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index a51049e85..4d9e0571a 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -777,7 +777,8 @@ void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations float weapSpeed; if (weaptype != MWMechanics::WeapType_HandToHand - && weaptype != MWMechanics::WeapType_Spell) + && weaptype != MWMechanics::WeapType_Spell + && weaptype != MWMechanics::WeapType_None) { weapon = weaponSlot->get()->mBase; weapSpeed = weapon->mData.mSpeed; From 253036abee5dfb609e716852ce8eb30d7a6b63d5 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 28 Aug 2014 02:55:22 +0200 Subject: [PATCH 7/7] Play VFX_Soul_Trap on successful soul trap --- apps/openmw/mwmechanics/actors.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 26332e47c..a94f26cd4 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -248,6 +248,12 @@ namespace MWMechanics if (caster.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Soul_Trap"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(mCreature.getRefData().getPosition().pos)); } };