diff --git a/CHANGELOG.md b/CHANGELOG.md index 83892500dd..abb10007ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ------ Bug #2623: Snowy Granius doesn't prioritize conjuration spells + Bug #3438: NPCs can't hit bull netch with melee weapons Bug #3842: Body part skeletons override the main skeleton Bug #4127: Weapon animation looks choppy Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 40d57cbb6e..e5499f6680 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -252,13 +252,6 @@ namespace MWBase virtual float getMaxActivationDistance() const = 0; - /// Returns a pointer to the object the provided object would hit (if within the - /// specified distance), and the point where the hit occurs. This will attempt to - /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. - virtual std::pair getHitContact( - const MWWorld::ConstPtr& ptr, float distance, std::vector& targets) - = 0; - virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) = 0; ///< Adjust position after load to be on ground. Must be called after model load. /// @param force do this even if the ptr is flying @@ -546,9 +539,6 @@ namespace MWBase const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) = 0; - /// Return the distance between actor's weapon and target's collision box. - virtual float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0; - virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index df1ada96f4..36241b02d2 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -242,24 +242,10 @@ namespace MWClass if (!weapon.isEmpty()) dist *= weapon.get()->mBase->mData.mReach; - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit - // result. - std::vector targetActors; - getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors); - - std::pair result - = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors); + const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; - const MWWorld::Class& othercls = result.first.getClass(); - if (!othercls.isActor()) // Can't hit non-actors - return true; - - MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(result.first); - if (otherstats.isDead()) // Can't hit dead actors - return true; - // Note that earlier we returned true in spite of an apparent failure to hit anything alive. // This is because hitting nothing is not a "miss" and should be handled as such character controller-side. victim = result.first; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 1e7dae3600..91601513a8 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -569,25 +569,10 @@ namespace MWClass * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach : store.find("fHandToHandReach")->mValue.getFloat()); - // 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 (ptr != MWMechanics::getPlayer()) - getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors); - - // TODO: Use second to work out the hit angle - std::pair result = world->getHitContact(ptr, dist, targetActors); + const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; - const MWWorld::Class& othercls = result.first.getClass(); - if (!othercls.isActor()) // Can't hit non-actors - return true; - - MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(result.first); - if (otherstats.isDead()) // Can't hit dead actors - return true; - // Note that earlier we returned true in spite of an apparent failure to hit anything alive. // This is because hitting nothing is not a "miss" and should be handled as such character controller-side. victim = result.first; diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index dbe83eab42..4c6ea42d36 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -23,6 +23,7 @@ #include "actorutil.hpp" #include "aicombataction.hpp" #include "character.hpp" +#include "combat.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "pathgrid.hpp" @@ -242,7 +243,7 @@ namespace MWMechanics const osg::Vec3f vActorPos(pos.asVec3()); const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); - float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); + float distToTarget = getDistanceToBounds(actor, target); storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 1c9c0c1dee..02279859b5 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -551,4 +551,102 @@ namespace MWMechanics return distanceIgnoreZ(lhs, rhs); return distance(lhs, rhs); } + + float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + { + osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + MWBase::World* world = MWBase::Environment::get().getWorld(); + + float dist = (targetPos - actorPos).length(); + dist -= world->getHalfExtents(actor).y(); + dist -= world->getHalfExtents(target).y(); + return dist; + } + + std::pair getHitContact(const MWWorld::Ptr& actor, float reach) + { + // Lasciate ogne speranza, voi ch'entrate + MWWorld::Ptr result; + osg::Vec3f hitPos; + float minDist = std::numeric_limits::max(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::Store& store = world->getStore().get(); + + const ESM::Position& posdata = actor.getRefData().getPosition(); + const osg::Vec3f actorPos(posdata.asVec3()); + + // Morrowind uses body orientation or camera orientation if available + // The difference between that and this is subtle + osg::Quat actorRot + = osg::Quat(posdata.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0, 0, -1)); + + const float fCombatAngleXY = store.find("fCombatAngleXY")->mValue.getFloat(); + const float fCombatAngleZ = store.find("fCombatAngleZ")->mValue.getFloat(); + const float combatAngleXYcos = std::cos(osg::DegreesToRadians(fCombatAngleXY)); + const float combatAngleZcos = std::cos(osg::DegreesToRadians(fCombatAngleZ)); + + // The player can target any active actor, non-playable actors only target their targets + std::vector targets; + if (actor != getPlayer()) + actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targets); + else + MWBase::Environment::get().getMechanicsManager()->getActorsInRange( + actorPos, Settings::game().mActorsProcessingRange, targets); + + for (MWWorld::Ptr& target : targets) + { + if (actor == target || target.getClass().getCreatureStats(target).isDead()) + continue; + float dist = getDistanceToBounds(actor, target); + osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); + osg::Vec3f dirToTarget = targetPos - actorPos; + if (dist >= reach || dist >= minDist || std::abs(dirToTarget.z()) >= reach) + continue; + + dirToTarget.normalize(); + + // The idea is to use fCombatAngleXY and fCombatAngleZ as tolerance angles + // in XY and YZ planes of the coordinate system where the actor's orientation + // corresponds to (0, 1, 0) vector. This is not exactly what Morrowind does + // but Morrowind does something (even more) stupid here + osg::Vec3f hitDir = actorRot.inverse() * dirToTarget; + if (combatAngleXYcos * std::abs(hitDir.x()) > hitDir.y()) + continue; + + // Nice cliff racer hack Todd + if (combatAngleZcos * std::abs(hitDir.z()) > hitDir.y() && !MWMechanics::canActorMoveByZAxis(target)) + continue; + + // Gotta use physics somehow! + if (!world->getLOS(actor, target)) + continue; + + minDist = dist; + result = target; + } + + // This hit position is currently used for spawning the blood effect. + // Morrowind does this elsewhere, but roughly at the same time + // and it would be hard to track the original hit results outside of this function + // without code duplication + // The idea is to use a random point on a plane in front of the target + // that is defined by its width and height + if (!result.isEmpty()) + { + osg::Vec3f resultPos(result.getRefData().getPosition().asVec3()); + osg::Vec3f dirToActor = actorPos - resultPos; + dirToActor.normalize(); + + hitPos = resultPos + dirToActor * world->getHalfExtents(result).y(); + // -25% to 25% of width + float xOffset = Misc::Rng::deviate(0.f, 0.25f, world->getPrng()); + // 20% to 100% of height + float zOffset = Misc::Rng::deviate(0.6f, 0.4f, world->getPrng()); + hitPos.x() += world->getHalfExtents(result).x() * 2.f * xOffset; + hitPos.z() += world->getHalfExtents(result).z() * 2.f * zOffset; + } + + return std::make_pair(result, hitPos); + } } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 2e7caf6189..515d2e406c 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MECHANICS_COMBAT_H #define OPENMW_MECHANICS_COMBAT_H +#include + namespace osg { class Vec3f; @@ -59,6 +61,11 @@ namespace MWMechanics float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); + // Cursed distance calculation used for combat proximity and hit checks in Morrowind + float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + + // Similarly cursed hit target selection + std::pair getHitContact(const MWWorld::Ptr& actor, float reach); } #endif diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 2d756fedc8..2196834a50 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -192,93 +192,6 @@ namespace MWPhysics return true; } - std::pair PhysicsSystem::getHitContact(const MWWorld::ConstPtr& actor, - const osg::Vec3f& origin, const osg::Quat& orient, float queryDistance, std::vector& targets) - { - // First of all, try to hit where you aim to - int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - RayCastingResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, - targets, hitmask, CollisionType_Actor); - - if (result.mHit) - { - reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal)); - return std::make_pair(result.mHitObject, result.mHitPos); - } - - // Use cone shape as fallback - const MWWorld::Store& store - = MWBase::Environment::get().getESMStore()->get(); - - btConeShape shape(osg::DegreesToRadians(store.find("fCombatAngleXY")->mValue.getFloat() / 2.0f), queryDistance); - shape.setLocalScaling(btVector3( - 1, 1, osg::DegreesToRadians(store.find("fCombatAngleZ")->mValue.getFloat() / 2.0f) / shape.getRadius())); - - // The shape origin is its center, so we have to move it forward by half the length. The - // real origin will be provided to getFilteredContact to find the closest. - osg::Vec3f center = origin + (orient * osg::Vec3f(0.0f, queryDistance * 0.5f, 0.0f)); - - btCollisionObject object; - object.setCollisionShape(&shape); - object.setWorldTransform(btTransform(Misc::Convert::toBullet(orient), Misc::Convert::toBullet(center))); - - const btCollisionObject* me = nullptr; - std::vector targetCollisionObjects; - - const Actor* physactor = getActor(actor); - if (physactor) - me = physactor->getCollisionObject(); - - if (!targets.empty()) - { - for (MWWorld::Ptr& target : targets) - { - const Actor* targetActor = getActor(target); - if (targetActor) - targetCollisionObjects.push_back(targetActor->getCollisionObject()); - } - } - - DeepestNotMeContactTestResultCallback resultCallback( - me, targetCollisionObjects, Misc::Convert::toBullet(origin)); - resultCallback.m_collisionFilterGroup = CollisionType_Actor; - resultCallback.m_collisionFilterMask - = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - mTaskScheduler->contactTest(&object, resultCallback); - - if (resultCallback.mObject) - { - PtrHolder* holder = static_cast(resultCallback.mObject->getUserPointer()); - if (holder) - { - reportCollision(resultCallback.mContactPoint, resultCallback.mContactNormal); - return std::make_pair(holder->getPtr(), Misc::Convert::toOsg(resultCallback.mContactPoint)); - } - } - return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); - } - - float PhysicsSystem::getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const - { - btCollisionObject* targetCollisionObj = nullptr; - const Actor* actor = getActor(target); - if (actor) - targetCollisionObj = actor->getCollisionObject(); - if (!targetCollisionObj) - return 0.f; - - btTransform rayFrom; - rayFrom.setIdentity(); - rayFrom.setOrigin(Misc::Convert::toBullet(point)); - - auto hitpoint = mTaskScheduler->getHitPoint(rayFrom, targetCollisionObj); - if (hitpoint) - return (point - Misc::Convert::toOsg(*hitpoint)).length(); - - // didn't hit the target. this could happen if point is already inside the collision box - return 0.f; - } - RayCastingResult PhysicsSystem::castRay(const osg::Vec3f& from, const osg::Vec3f& to, const MWWorld::ConstPtr& ignore, const std::vector& targets, int mask, int group) const { diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index e4c1b63776..ad56581eb3 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -207,15 +207,6 @@ namespace MWPhysics const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const; osg::Vec3f traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, float maxHeight); - std::pair getHitContact(const MWWorld::ConstPtr& actor, const osg::Vec3f& origin, - const osg::Quat& orientation, float queryDistance, std::vector& targets); - - /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the - /// target vector hits the collision shape and then calculates distance from the intersection point. - /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be - /// successful. \note Only Actor targets are supported at the moment. - float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const override; - /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all /// other actors. RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index 4a56e9bf33..6b1a743d54 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -23,12 +23,6 @@ namespace MWPhysics public: virtual ~RayCastingInterface() = default; - /// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the - /// target vector hits the collision shape and then calculates distance from the intersection point. - /// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be - /// successful. \note Only Actor targets are supported at the moment. - virtual float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const = 0; - /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all /// other actors. virtual RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to, diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 748187d868..02e4d066bc 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1037,44 +1037,6 @@ namespace MWWorld return osg::Matrixf::translate(actor.getRefData().getPosition().asVec3()); } - std::pair World::getHitContact( - const MWWorld::ConstPtr& ptr, float distance, std::vector& targets) - { - const ESM::Position& posdata = ptr.getRefData().getPosition(); - - osg::Quat rot - = osg::Quat(posdata.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0, 0, -1)); - - osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); - - // the origin of hitbox is an actor's front, not center - distance += halfExtents.y(); - - // special cased for better aiming with the camera - // if we do not hit anything, will use the default approach as fallback - if (ptr == getPlayerPtr()) - { - osg::Vec3f pos = getActorHeadTransform(ptr).getTrans(); - - std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); - if (!result.first.isEmpty()) - return std::make_pair(result.first, result.second); - } - - osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); - - // general case, compatible with all types of different creatures - // note: we intentionally do *not* use the collision box offset here, this is required to make - // some flying creatures work that have their collision box offset in the air - pos.z() += halfExtents.z(); - - std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); - if (result.first.isEmpty()) - return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); - - return std::make_pair(result.first, result.second); - } - void World::deleteObject(const Ptr& ptr) { if (!ptr.getRefData().isDeleted() && ptr.getContainerStore() == nullptr) @@ -3010,12 +2972,12 @@ namespace MWWorld } else { - // For actor targets, we want to use hit contact with bounding boxes. + // For actor targets, we want to use melee hit contact. // 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); + const std::pair result1 = MWMechanics::getHitContact(actor, fCombatDistance); // Get the target to use for "on touch" effects, using the facing direction from Head node osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); @@ -3728,15 +3690,6 @@ namespace MWWorld return (targetPos - weaponPos); } - float World::getHitDistance(const ConstPtr& actor, const ConstPtr& target) - { - osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); - osg::Vec3f halfExtents = mPhysics->getHalfExtents(actor); - weaponPos.z() += halfExtents.z(); - - return mPhysics->getHitDistance(weaponPos, target) - halfExtents.y(); - } - void preload(MWWorld::Scene* scene, const ESMStore& store, const ESM::RefId& obj) { if (obj.empty()) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 6bf256b083..46043afe46 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -345,12 +345,6 @@ namespace MWWorld float getDistanceToFacedObject() override; - /// Returns a pointer to the object the provided object would hit (if within the - /// specified distance), and the point where the hit occurs. This will attempt to - /// use the "Head" node as a basis. - std::pair getHitContact( - const MWWorld::ConstPtr& ptr, float distance, std::vector& targets) override; - /// @note No-op for items in containers. Use ContainerStore::removeItem instead. void deleteObject(const Ptr& ptr) override; @@ -627,9 +621,6 @@ namespace MWWorld osg::Vec3f aimToTarget( const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) override; - /// Return the distance between actor's weapon and target's collision box. - float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) override; - bool isPlayerInJail() const override; void setPlayerTraveling(bool traveling) override;