diff --git a/CHANGELOG.md b/CHANGELOG.md index 5838e11c77..9d09f00b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Bug #7641: loopgroup loops the animation one time too many for actors Bug #7642: Items in repair and recharge menus aren't sorted alphabetically Bug #7643: Can't enchant items with constant effect on self magic effects for non-player character + Bug #7646: Follower voices pain sounds when attacked with magic Bug #7647: NPC walk cycle bugs after greeting player Bug #7654: Tooltips for enchantments with invalid effects cause crashes Bug #7660: Some inconsistencies regarding Invisibility breaking diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 6e611aa88f..f3cea83224 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -69,7 +69,8 @@ namespace MWBase // TODO: notify LuaManager about other events // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, - // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) = 0; + // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, + // DamageSourceType sourceType) = 0; struct InputEvent { diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index c5ce954baa..052315ce54 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -284,7 +284,8 @@ namespace MWClass if (!success) { - victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); + victim.getClass().onHit( + victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -345,24 +346,35 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + victim.getClass().onHit( + victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + // Self defense + bool setOnPcHitMe = true; + // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) + { stats.setAttacked(true); - // Self defense - bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. - - // No retaliation for totally static creatures (they have no movement or attacks anyway) - if (isMobile(ptr) && !attacker.isEmpty()) - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + // No retaliation for totally static creatures (they have no movement or attacks anyway) + if (isMobile(ptr)) + { + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } + } // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index bd7101e93d..b407852242 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -67,7 +67,8 @@ namespace MWClass const osg::Vec3f& hitPosition, bool success) const override; void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const override; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index cbb1f4b307..8e2bf1b351 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -597,7 +597,8 @@ namespace MWClass float damage = 0.0f; if (!success) { - othercls.onHit(victim, damage, false, weapon, ptr, osg::Vec3f(), false); + othercls.onHit( + victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); return; @@ -670,24 +671,29 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); } void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); - // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) { stats.setAttacked(true); - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } // Attacker and target store each other as hitattemptactor if they have no one stored yet diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index eb8cafc9d1..ca0d0ac95d 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -82,7 +82,8 @@ namespace MWClass const osg::Vec3f& hitPosition, bool success) const override; void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const override; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const override; void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 02279859b5..3f17df96fd 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -10,6 +10,7 @@ #include #include +#include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -230,7 +231,8 @@ namespace MWMechanics if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); + victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, + MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } @@ -286,7 +288,8 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1); } - victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); + victim.getClass().onHit( + victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); } } @@ -649,4 +652,26 @@ namespace MWMechanics return std::make_pair(result, hitPos); } + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain) + { + const MWWorld::Ptr& player = getPlayer(); + if (attacker != player) + return false; + + std::set followersAttacker; + MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(attacker, followersAttacker); + if (followersAttacker.find(target) == followersAttacker.end()) + return false; + + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); + statsTarget.friendlyHit(); + if (statsTarget.getFriendlyHits() >= 4) + return false; + + if (complain) + MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); + return true; + } + } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 515d2e406c..92033c7e77 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -66,6 +66,8 @@ namespace MWMechanics // Similarly cursed hit target selection std::pair getHitContact(const MWWorld::Ptr& actor, float reach); + + bool friendlyHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& target, bool complain); } #endif diff --git a/apps/openmw/mwmechanics/damagesourcetype.hpp b/apps/openmw/mwmechanics/damagesourcetype.hpp new file mode 100644 index 0000000000..e140a8106f --- /dev/null +++ b/apps/openmw/mwmechanics/damagesourcetype.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H +#define OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H + +namespace MWMechanics +{ + enum class DamageSourceType + { + Unspecified, + Melee, + Ranged, + Magical, + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 3a253d9dc3..d4fd63309f 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1466,26 +1466,10 @@ namespace MWMechanics if (target == player || !attacker.getClass().isActor()) return false; - MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); - if (attacker == player) - { - std::set followersAttacker; - getActorsSidingWith(attacker, followersAttacker); - if (followersAttacker.find(target) != followersAttacker.end()) - { - statsTarget.friendlyHit(); - - if (statsTarget.getFriendlyHits() < 4) - { - MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit")); - return false; - } - } - } - if (canCommitCrimeAgainst(target, attacker)) commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); + MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); AiSequence& seq = statsTarget.getAiSequence(); if (!attacker.isEmpty() diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 334bdf8839..e7146f3e7a 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -353,7 +353,8 @@ namespace // Notify the target actor they've been hit bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); + target.getClass().onHit( + target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical); // Apply resistances if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) { diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index e6080ce447..5fbda6d570 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -124,7 +124,7 @@ namespace MWWorld } void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, - const osg::Vec3f& hitPosition, bool successful) const + const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 7b7e9135ba..87e70b3198 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -13,6 +13,8 @@ #include "ptr.hpp" #include "../mwmechanics/aisetting.hpp" +#include "../mwmechanics/damagesourcetype.hpp" + #include #include @@ -142,11 +144,12 @@ namespace MWWorld /// (default implementation: throw an exception) virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const; + const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + const MWMechanics::DamageSourceType sourceType) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the - /// actor responsible for the attack, and \a successful specifies if the hit is - /// successful or not. + /// actor responsible for the attack. \a successful specifies if the hit is + /// successful or not. \a sourceType classifies the damage source. virtual void block(const Ptr& ptr) const; ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield