From 3d1daaebab4d7100afbcca8c8d22c0571356129a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 28 Jun 2018 16:58:51 +0400 Subject: [PATCH 1/2] Rework manual spellcasting (e.g. via scripts) --- apps/openmw/CMakeLists.txt | 4 +- apps/openmw/mwbase/mechanicsmanager.hpp | 3 + apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwmechanics/actors.cpp | 16 ++++ apps/openmw/mwmechanics/actors.hpp | 3 + apps/openmw/mwmechanics/aicast.cpp | 84 +++++++++++++++++ apps/openmw/mwmechanics/aicast.hpp | 37 ++++++++ apps/openmw/mwmechanics/aipackage.hpp | 3 +- apps/openmw/mwmechanics/aisequence.cpp | 20 +++-- apps/openmw/mwmechanics/character.cpp | 34 +++++-- apps/openmw/mwmechanics/character.hpp | 3 + .../mwmechanics/mechanicsmanagerimp.cpp | 11 +++ .../mwmechanics/mechanicsmanagerimp.hpp | 4 + apps/openmw/mwmechanics/spellcasting.cpp | 15 +++- apps/openmw/mwmechanics/spellcasting.hpp | 5 +- apps/openmw/mwscript/miscextensions.cpp | 23 ++++- apps/openmw/mwworld/worldimp.cpp | 90 +++++++++++-------- apps/openmw/mwworld/worldimp.hpp | 2 +- 18 files changed, 298 insertions(+), 61 deletions(-) create mode 100644 apps/openmw/mwmechanics/aicast.cpp create mode 100644 apps/openmw/mwmechanics/aicast.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index e8a958d45..0092712db 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -81,9 +81,9 @@ add_openmw_dir (mwclass add_openmw_dir (mwmechanics mechanicsmanagerimp stat creaturestats magiceffects movement actorutil drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe - aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting + aicast aiescort aiface aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate coordinateconverter trading aiface weaponpriority spellpriority + character actors objects aistate coordinateconverter trading weaponpriority spellpriority ) add_openmw_dir (mwstate diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index f15a86918..995c8d736 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -225,9 +225,12 @@ namespace MWBase /// Resurrects the player if necessary virtual void keepPlayerAlive() = 0; + virtual bool isCastingSpell (const MWWorld::Ptr& ptr) const = 0; virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const = 0; virtual bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const = 0; + virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0; + /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index e07c1ffd9..97993e984 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -487,7 +487,7 @@ namespace MWBase */ virtual bool startSpellCast (const MWWorld::Ptr& actor) = 0; - virtual void castSpell (const MWWorld::Ptr& actor) = 0; + virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index a225014b4..36ba77cfb 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1157,6 +1157,13 @@ namespace MWMechanics } } + void Actors::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + { + PtrActorMap::iterator iter = mActors.find(ptr); + if(iter != mActors.end()) + iter->second->getCharacterController()->castSpell(spellId, manualSpell); + } + bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) { if (!actor.getClass().isActor()) @@ -1968,6 +1975,15 @@ namespace MWMechanics return it->second->getCharacterController()->isReadyToBlock(); } + bool Actors::isCastingSpell(const MWWorld::Ptr &ptr) const + { + PtrActorMap::const_iterator it = mActors.find(ptr); + if (it == mActors.end()) + return false; + + return it->second->getCharacterController()->isCastingSpell(); + } + bool Actors::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 0de1f4d6c..492ff1e2e 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -77,6 +77,8 @@ namespace MWMechanics /// /// \note Ignored, if \a ptr is not a registered actor. + void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); + void updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); ///< Updates an actor with a new Ptr @@ -161,6 +163,7 @@ namespace MWMechanics void clear(); // Clear death counter + bool isCastingSpell(const MWWorld::Ptr& ptr) const; bool isReadyToBlock(const MWWorld::Ptr& ptr) const; bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp new file mode 100644 index 000000000..48cb17f6d --- /dev/null +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -0,0 +1,84 @@ +#include "aicast.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" + +#include "../mwworld/class.hpp" + +#include "aicombataction.hpp" +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "steering.hpp" + +MWMechanics::AiCast::AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell) + : mTargetId(targetId), mSpellId(spellId), mCasting(false), mManual(manualSpell), mDistance(0) +{ + ActionSpell action = ActionSpell(spellId); + bool isRanged; + mDistance = action.getCombatRange(isRanged); +} + +MWMechanics::AiPackage *MWMechanics::AiCast::clone() const +{ + return new AiCast(*this); +} + +bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& characterController, MWMechanics::AiState& state, float duration) +{ + MWWorld::Ptr target; + if (actor.getCellRef().getRefId() == mTargetId) + { + // If the target has the same ID as caster, consider that actor casts spell with Self range. + target = actor; + } + else + { + target = getTarget(); + if (!target) + return true; + + if (!mManual && !pathTo(actor, target.getRefData().getPosition().pos, duration, mDistance)) + { + return false; + } + + osg::Vec3f dir = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3(); + bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); + turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); + + if (!turned) + return false; + } + + // Check if the actor is already casting another spell + bool isCasting = MWBase::Environment::get().getMechanicsManager()->isCastingSpell(actor); + if (isCasting && !mCasting) + return false; + + if (!mCasting) + { + MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); + mCasting = true; + return false; + } + + // Finish package, if actor finished spellcasting + return !isCasting; +} + +MWWorld::Ptr MWMechanics::AiCast::getTarget() const +{ + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetId, false); + + return target; +} + +int MWMechanics::AiCast::getTypeId() const +{ + return AiPackage::TypeIdCast; +} + +unsigned int MWMechanics::AiCast::getPriority() const +{ + return 3; +} diff --git a/apps/openmw/mwmechanics/aicast.hpp b/apps/openmw/mwmechanics/aicast.hpp new file mode 100644 index 000000000..70a8a6bbd --- /dev/null +++ b/apps/openmw/mwmechanics/aicast.hpp @@ -0,0 +1,37 @@ +#ifndef GAME_MWMECHANICS_AICAST_H +#define GAME_MWMECHANICS_AICAST_H + +#include "../mwbase/world.hpp" + +#include "aipackage.hpp" + +namespace MWMechanics +{ + /// AiPackage which makes an actor to cast given spell. + class AiCast : public AiPackage { + public: + AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell=false); + + virtual AiPackage *clone() const; + + virtual bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration); + + virtual int getTypeId() const; + + virtual MWWorld::Ptr getTarget() const; + + virtual unsigned int getPriority() const; + + virtual bool canCancel() const { return false; } + virtual bool shouldCancelPreviousAi() const { return false; } + + private: + std::string mTargetId; + std::string mSpellId; + bool mCasting; + bool mManual; + float mDistance; + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 2b685accc..f1941ff1d 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -49,7 +49,8 @@ namespace MWMechanics TypeIdAvoidDoor = 7, TypeIdFace = 8, TypeIdBreathe = 9, - TypeIdInternalTravel = 10 + TypeIdInternalTravel = 10, + TypeIdCast = 11 }; ///Default constructor diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 86cc2bbdc..085174820 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -180,12 +180,8 @@ bool AiSequence::isPackageDone() const bool isActualAiPackage(int packageTypeId) { - return (packageTypeId != AiPackage::TypeIdCombat - && packageTypeId != AiPackage::TypeIdPursue - && packageTypeId != AiPackage::TypeIdAvoidDoor - && packageTypeId != AiPackage::TypeIdFace - && packageTypeId != AiPackage::TypeIdBreathe - && packageTypeId != AiPackage::TypeIdInternalTravel); + return (packageTypeId >= AiPackage::TypeIdWander && + packageTypeId <= AiPackage::TypeIdActivate); } void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration) @@ -308,7 +304,7 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if (isActualAiPackage(package.getTypeId())) stopCombat(); - // We should return a wandering actor back after combat or pursuit. + // We should return a wandering actor back after combat, casting or pursuit. // The same thing for actors without AI packages. // Also there is no point to stack return packages. int currentTypeId = getTypeId(); @@ -316,7 +312,8 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if (currentTypeId <= MWMechanics::AiPackage::TypeIdWander && !hasPackage(MWMechanics::AiPackage::TypeIdInternalTravel) && (newTypeId <= MWMechanics::AiPackage::TypeIdCombat - || newTypeId == MWMechanics::AiPackage::TypeIdPursue)) + || newTypeId == MWMechanics::AiPackage::TypeIdPursue + || newTypeId == MWMechanics::AiPackage::TypeIdCast)) { osg::Vec3f dest; if (currentTypeId == MWMechanics::AiPackage::TypeIdWander) @@ -352,6 +349,13 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo // insert new package in correct place depending on priority for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) { + // We should keep current AiCast package, if we try to add a new one. + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdCast && + package.getTypeId() == MWMechanics::AiPackage::TypeIdCast) + { + continue; + } + if((*it)->getPriority() <= package.getPriority()) { mPackages.insert(it,package.clone()); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index c36129c4e..aaa6c7aeb 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -39,6 +39,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" +#include "aicombataction.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "creaturestats.hpp" @@ -794,6 +795,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mSecondsOfRunning(0) , mTurnAnimationThreshold(0) , mAttackingOrSpell(false) + , mCastingManualSpell(false) , mTimeUntilWake(0.f) { if(!mAnimation) @@ -1007,7 +1009,8 @@ void CharacterController::handleTextKey(const std::string &groupname, const std: // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. && evt.compare(off, len, mAttackType + " release") == 0) { - MWBase::Environment::get().getWorld()->castSpell(mPtr); + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); + mCastingManualSpell = false; } else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) @@ -1088,13 +1091,18 @@ bool CharacterController::updateCreatureState() if (weapType == WeapType_Spell) { const std::string spellid = stats.getSpells().getSelectedSpell(); - if (!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); + + if (!spellid.empty() && canCast) { - MWMechanics::CastSpell cast(mPtr, NULL); + MWMechanics::CastSpell cast(mPtr, NULL, false, mCastingManualSpell); cast.playSpellCastingEffects(spellid); if (!mAnimation->hasAnimation("spellcast")) - MWBase::Environment::get().getWorld()->castSpell(mPtr); // No "release" text key to use, so cast immediately + { + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately + mCastingManualSpell = false; + } else { const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); @@ -1367,10 +1375,11 @@ bool CharacterController::updateWeaponState() stats.getSpells().setSelectedSpell(selectedSpell); } std::string spellid = stats.getSpells().getSelectedSpell(); + bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); - if(!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + if(!spellid.empty() && canCast) { - MWMechanics::CastSpell cast(mPtr, NULL); + MWMechanics::CastSpell cast(mPtr, NULL, false, mCastingManualSpell); cast.playSpellCastingEffects(spellid); const ESM::Spell *spell = store.get().find(spellid); @@ -2377,6 +2386,11 @@ bool CharacterController::isAttackPrepairing() const mUpperBodyState == UpperCharState_MinAttackToMaxAttack; } +bool CharacterController::isCastingSpell() const +{ + return mCastingManualSpell || mUpperBodyState == UpperCharState_CastingSpell; +} + bool CharacterController::isReadyToBlock() const { return updateCarriedLeftVisible(mWeaponType); @@ -2440,6 +2454,14 @@ void CharacterController::setAttackingOrSpell(bool attackingOrSpell) mAttackingOrSpell = attackingOrSpell; } +void CharacterController::castSpell(const std::string spellId, bool manualSpell) +{ + mAttackingOrSpell = true; + mCastingManualSpell = manualSpell; + ActionSpell action = ActionSpell(spellId); + action.prepare(mPtr); +} + void CharacterController::setAIAttackType(const std::string& attackType) { mAttackType = attackType; diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 381cf71a5..84630a479 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -204,6 +204,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener std::string mAttackType; // slash, chop or thrust bool mAttackingOrSpell; + bool mCastingManualSpell; float mTimeUntilWake; @@ -276,6 +277,7 @@ public: void forceStateUpdate(); bool isAttackPrepairing() const; + bool isCastingSpell() const; bool isReadyToBlock() const; bool isKnockedDown() const; bool isKnockedOut() const; @@ -286,6 +288,7 @@ public: bool isAttackingOrSpell() const; void setAttackingOrSpell(bool attackingOrSpell); + void castSpell(const std::string spellId, bool manualSpell=false); void setAIAttackType(const std::string& attackType); static void setAttackTypeRandomly(std::string& attackType); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c2f7a664c..7814b4a91 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -253,6 +253,12 @@ namespace MWMechanics mObjects.addObject(ptr); } + void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + { + if(ptr.getClass().isActor()) + mActors.castSpell(ptr, spellId, manualSpell); + } + void MechanicsManager::remove(const MWWorld::Ptr& ptr) { if(ptr == mWatched) @@ -1758,6 +1764,11 @@ namespace MWMechanics stats.resurrect(); } + bool MechanicsManager::isCastingSpell(const MWWorld::Ptr &ptr) const + { + return mActors.isCastingSpell(ptr); + } + bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr &ptr) const { return mActors.isReadyToBlock(ptr); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 07dfd81ff..676a75caf 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -190,10 +190,14 @@ namespace MWMechanics virtual void keepPlayerAlive(); + virtual bool isCastingSpell (const MWWorld::Ptr& ptr) const; + virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const; /// Is \a ptr casting spell or using weapon now? virtual bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const; + virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); + /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer); diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index f1997e8d7..4557a52df 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -315,13 +315,14 @@ namespace MWMechanics return true; } - CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile) + CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool isScripted) : mCaster(caster) , mTarget(target) , mStack(false) , mHitPosition(0,0,0) , mAlwaysSucceed(false) , mFromProjectile(fromProjectile) + , mIsScripted(isScripted) { } @@ -863,7 +864,7 @@ namespace MWMechanics bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if (mCaster.getClass().isActor() && !mAlwaysSucceed) + if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mIsScripted) { school = getSpellSchool(spell, mCaster); @@ -910,7 +911,7 @@ namespace MWMechanics stats.getSpells().usePower(spell); } - if (mCaster == getPlayer() && spellIncreasesSkill(spell)) + if (mCaster == getPlayer() && spellIncreasesSkill()) mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0); @@ -1034,6 +1035,14 @@ namespace MWMechanics } } + bool CastSpell::spellIncreasesSkill() + { + if (mIsScripted) + return false; + + return MWMechanics::spellIncreasesSkill(mId); + } + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) { /* diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 07c5b8477..5e69b4696 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -88,9 +88,10 @@ namespace MWMechanics osg::Vec3f mHitPosition; // Used for spawning area orb bool mAlwaysSucceed; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) + bool mIsScripted; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) public: - CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false); + CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool isScripted=false); bool cast (const ESM::Spell* spell); @@ -108,6 +109,8 @@ namespace MWMechanics void playSpellCastingEffects(const std::string &spellid); + bool spellIncreasesSkill(); + /// Launch a bolt with the given effects. void launchMagicBolt (); diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 3fa66af10..30d0c6fee 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -25,6 +25,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwmechanics/aicast.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" @@ -1056,15 +1057,31 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string spell = runtime.getStringLiteral (runtime[0].mInteger); + std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string targetId = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral (runtime[0].mInteger)); runtime.pop(); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find (spellId); + if (spell && spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) + { + runtime.getContext().report("spellcasting failed: you can cast only spells and powers."); + return; + } + + // Obviously we can not use casting animation for player here + if (ptr.getClass().isActor() && ptr != MWMechanics::getPlayer()) + { + MWMechanics::AiCast castPackage(targetId, spellId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + + return; + } + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr (targetId, false); - MWMechanics::CastSpell cast(ptr, target); + MWMechanics::CastSpell cast(ptr, target, false, true); cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); @@ -1082,7 +1099,7 @@ namespace MWScript std::string spell = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - MWMechanics::CastSpell cast(ptr, ptr); + MWMechanics::CastSpell cast(ptr, ptr, false, true); cast.mHitPosition = ptr.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 5d554176a..6bf964d9b 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2771,13 +2771,13 @@ namespace MWWorld return !fail; } - void World::castSpell(const Ptr &actor) + void World::castSpell(const Ptr &actor, bool manualSpell) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; - if (!actor.isEmpty() && actor != MWMechanics::getPlayer()) + if (!actor.isEmpty() && actor != MWMechanics::getPlayer() && !manualSpell) actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors); const float fCombatDistance = getStore().get().find("fCombatDistance")->getFloat(); @@ -2795,51 +2795,71 @@ namespace MWWorld if (target.isEmpty()) { - // For actor targets, we want to use hit contact with bounding boxes. - // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. - // For object targets, we want the detailed shapes (rendering raycast). - // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. - std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); + // For scripted spells we should not use hit contact + if (manualSpell) + { + // Actors that are targeted by this actor's Follow or Escort packages also side with them + if (actor != MWMechanics::getPlayer()) + { + const MWMechanics::CreatureStats &stats = actor.getClass().getCreatureStats(actor); + for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdCast) + { + target = (*it)->getTarget(); + break; + } + } + } + } + else + { + // For actor targets, we want to use hit contact with bounding boxes. + // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. + // For object targets, we want the detailed shapes (rendering raycast). + // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. + std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); - // Get the target to use for "on touch" effects, using the facing direction from Head node - osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); + // Get the target to use for "on touch" effects, using the facing direction from Head node + osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); - osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); + osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) + * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); - osg::Vec3f direction = orient * osg::Vec3f(0,1,0); - float distance = getMaxActivationDistance(); - osg::Vec3f dest = origin + direction * distance; + osg::Vec3f direction = orient * osg::Vec3f(0,1,0); + float distance = getMaxActivationDistance(); + osg::Vec3f dest = origin + direction * distance; - MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); - float dist1 = std::numeric_limits::max(); - float dist2 = std::numeric_limits::max(); + float dist1 = std::numeric_limits::max(); + float dist2 = std::numeric_limits::max(); - if (!result1.first.isEmpty() && result1.first.getClass().isActor()) - dist1 = (origin - result1.second).length(); - if (result2.mHit) - dist2 = (origin - result2.mHitPointWorld).length(); + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + dist1 = (origin - result1.second).length(); + if (result2.mHit) + dist2 = (origin - result2.mHitPointWorld).length(); - if (!result1.first.isEmpty() && result1.first.getClass().isActor()) - { - target = result1.first; - hitPosition = result1.second; - if (dist1 > getMaxActivationDistance()) - target = NULL; - } - else if (result2.mHit) - { - target = result2.mHitObject; - hitPosition = result2.mHitPointWorld; - if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) - target = NULL; + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + { + target = result1.first; + hitPosition = result1.second; + if (dist1 > getMaxActivationDistance()) + target = NULL; + } + else if (result2.mHit) + { + target = result2.mHitObject; + hitPosition = result2.mHitPointWorld; + if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) + target = NULL; + } } } std::string selectedSpell = stats.getSpells().getSelectedSpell(); - MWMechanics::CastSpell cast(actor, target); + MWMechanics::CastSpell cast(actor, target, false, manualSpell); cast.mHitPosition = hitPosition; if (!selectedSpell.empty()) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 015a8b31b..941ab7a96 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -602,7 +602,7 @@ namespace MWWorld * @brief Cast the actual spell, should be called mid-animation * @param actor */ - void castSpell (const MWWorld::Ptr& actor) override; + void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, From 32bd294a8a135da6e6a8ce55e38449668d355238 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 12 Jul 2018 16:39:46 +0400 Subject: [PATCH 2/2] Add missing changelog entries --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4a02085..7fde57f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ------ Bug #1990: Sunrise/sunset not set correct + Bug #2131: Lustidrike's spell misses the player every time Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2455: Creatures attacks degrade armor @@ -65,7 +66,9 @@ Bug #4495: Crossbow animations blending is buggy Bug #4496: SpellTurnLeft and SpellTurnRight animation groups are unused Bug #4497: File names starting with x or X are not classified as animation + Bug #4503: Cast and ExplodeSpell commands increase alteration skill Feature #2606: Editor: Implemented (optional) case sensitive global search + Feature #3083: Play animation when NPC is casting spell via script Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #3641: Editor: Limit FPS in 3d preview window Feature #4222: 360° screenshots