From b4c699348f64c75c57624307f95da104eaffb5f9 Mon Sep 17 00:00:00 2001
From: Petr Mikheev <ptmikheev@gmail.com>
Date: Mon, 22 Jun 2020 02:03:38 +0200
Subject: [PATCH] Improved strafe movement

---
 apps/openmw/mwclass/creature.cpp          |   7 +-
 apps/openmw/mwclass/npc.cpp               |   7 +-
 apps/openmw/mwmechanics/character.cpp     | 108 ++++++++++++----------
 apps/openmw/mwmechanics/creaturestats.cpp |   2 +-
 apps/openmw/mwmechanics/creaturestats.hpp |   6 ++
 apps/openmw/mwmechanics/movement.hpp      |   2 +
 apps/openmw/mwrender/animation.cpp        |  59 +++++++++---
 apps/openmw/mwrender/animation.hpp        |  13 +++
 apps/openmw/mwrender/camera.cpp           |   3 +
 files/settings-default.cfg                |   6 ++
 10 files changed, 142 insertions(+), 71 deletions(-)

diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp
index 88defbaa09..24f25e508e 100644
--- a/apps/openmw/mwclass/creature.cpp
+++ b/apps/openmw/mwclass/creature.cpp
@@ -536,10 +536,11 @@ namespace MWClass
             moveSpeed = getSwimSpeed(ptr);
         else
             moveSpeed = getWalkSpeed(ptr);
-        if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0)
-            moveSpeed *= 0.75f;
 
-        moveSpeed *= ptr.getClass().getMovementSettings(ptr).mSpeedFactor;
+        const MWMechanics::Movement& movementSettings = ptr.getClass().getMovementSettings(ptr);
+        if (movementSettings.mIsStrafing)
+            moveSpeed *= 0.75f;
+        moveSpeed *= movementSettings.mSpeedFactor;
 
         return moveSpeed;
     }
diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp
index 319d5f0144..b7084aff04 100644
--- a/apps/openmw/mwclass/npc.cpp
+++ b/apps/openmw/mwclass/npc.cpp
@@ -966,13 +966,14 @@ namespace MWClass
             moveSpeed = getRunSpeed(ptr);
         else
             moveSpeed = getWalkSpeed(ptr);
-        if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0)
-            moveSpeed *= 0.75f;
 
         if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing)
             moveSpeed *= gmst.fWereWolfRunMult->mValue.getFloat();
 
-        moveSpeed *= ptr.getClass().getMovementSettings(ptr).mSpeedFactor;
+        const MWMechanics::Movement& movementSettings = ptr.getClass().getMovementSettings(ptr);
+        if (movementSettings.mIsStrafing)
+            moveSpeed *= 0.75f;
+        moveSpeed *= movementSettings.mSpeedFactor;
 
         return moveSpeed;
     }
diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp
index e4f870ed0d..24d1ebf368 100644
--- a/apps/openmw/mwmechanics/character.cpp
+++ b/apps/openmw/mwmechanics/character.cpp
@@ -1939,64 +1939,76 @@ void CharacterController::update(float duration, bool animationOnly)
         bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying;
         bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying;
         CreatureStats &stats = cls.getCreatureStats(mPtr);
+        Movement& movementSettings = cls.getMovementSettings(mPtr);
 
         //Force Jump Logic
 
-        bool isMoving = (std::abs(cls.getMovementSettings(mPtr).mPosition[0]) > .5 || std::abs(cls.getMovementSettings(mPtr).mPosition[1]) > .5);
+        bool isMoving = (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5);
         if(!inwater && !flying && solid)
         {
             //Force Jump
             if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump))
-            {
-                if(onground)
-                {
-                    cls.getMovementSettings(mPtr).mPosition[2] = 1;
-                }
-                else
-                    cls.getMovementSettings(mPtr).mPosition[2] = 0;
-            }
+                movementSettings.mPosition[2] = onground ? 1 : 0;
             //Force Move Jump, only jump if they're otherwise moving
             if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving)
-            {
-
-                if(onground)
-                {
-                    cls.getMovementSettings(mPtr).mPosition[2] = 1;
-                }
-                else
-                    cls.getMovementSettings(mPtr).mPosition[2] = 0;
-            }
+                movementSettings.mPosition[2] = onground ? 1 : 0;
         }
 
-        osg::Vec3f vec(cls.getMovementSettings(mPtr).asVec3());
+        osg::Vec3f rot = cls.getRotationVector(mPtr);
+        osg::Vec3f vec(movementSettings.asVec3());
         vec.normalize();
 
-        if(mHitState != CharState_None && mJumpState == JumpState_None)
-            vec = osg::Vec3f(0.f, 0.f, 0.f);
-        osg::Vec3f rot = cls.getRotationVector(mPtr);
-
-        speed = cls.getSpeed(mPtr);
-        float analogueMult = 1.f;
-        if(isPlayer)
+        float analogueMult = 1.0f;
+        if (isPlayer)
         {
+            // TODO: Move this code to mwinput.
             // Joystick analogue movement.
-            float xAxis = std::abs(cls.getMovementSettings(mPtr).mPosition[0]);
-            float yAxis = std::abs(cls.getMovementSettings(mPtr).mPosition[1]);
-            analogueMult = ((xAxis > yAxis) ? xAxis : yAxis);
-
-            // If Strafing, our max speed is slower so multiply by X axis instead.
-            if(std::abs(vec.x()/2.0f) > std::abs(vec.y()))
-                analogueMult = xAxis;
+            float xAxis = std::abs(movementSettings.mPosition[0]);
+            float yAxis = std::abs(movementSettings.mPosition[1]);
+            analogueMult = std::max(xAxis, yAxis);
 
             // Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used.
             if(!isrunning && !sneak && !flying && analogueMult <= 0.5f)
                 analogueMult *= 2.f;
+
+            movementSettings.mSpeedFactor = analogueMult;
         }
 
-        speed *= analogueMult;
+        float effectiveRotation = rot.z();
+        static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game");
+        static const float turnToMovementDirectionSpeedCoef = Settings::Manager::getFloat("turn to movement direction speed coef", "Game");
+        if (turnToMovementDirection && !(isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson()))
+        {
+            float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y());
+            movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater)
+                                           && std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f);
+            if (movementSettings.mIsStrafing)
+                targetMovementAngle = 0;
+            float delta = targetMovementAngle - stats.getSideMovementAngle();
+            float cosDelta = cosf(delta);
+            movementSettings.mSpeedFactor *= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn
+            float maxDelta = turnToMovementDirectionSpeedCoef * osg::PI * duration * (2.5f - cosDelta);
+            delta = std::min(delta, maxDelta);
+            delta = std::max(delta, -maxDelta);
+            stats.setSideMovementAngle(stats.getSideMovementAngle() + delta);
+            effectiveRotation += delta;
+        }
+        else
+            movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2;
+
+        mAnimation->setLegsYawRadians(stats.getSideMovementAngle());
+        if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater)
+            mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2);
+        else
+            mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4);
+
+        speed = cls.getSpeed(mPtr);
         vec.x() *= speed;
         vec.y() *= speed;
 
+        if(mHitState != CharState_None && mJumpState == JumpState_None)
+            vec = osg::Vec3f();
+
         CharacterState movestate = CharState_None;
         CharacterState idlestate = CharState_SpecialIdle;
         JumpingState jumpstate = JumpState_None;
@@ -2158,7 +2170,7 @@ void CharacterController::update(float duration, bool animationOnly)
 
             inJump = false;
 
-            if(std::abs(vec.x()/2.0f) > std::abs(vec.y()))
+            if (movementSettings.mIsStrafing)
             {
                 if(vec.x() > 0.0f)
                     movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight)
@@ -2169,18 +2181,18 @@ void CharacterController::update(float duration, bool animationOnly)
                                          : (sneak ? CharState_SneakLeft
                                                   : (isrunning ? CharState_RunLeft : CharState_WalkLeft)));
             }
-            else if(vec.y() != 0.0f)
+            else if (vec.length2() > 0.0f)
             {
-                if(vec.y() > 0.0f)
+                if (vec.y() >= 0.0f)
                     movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward)
                                          : (sneak ? CharState_SneakForward
                                                   : (isrunning ? CharState_RunForward : CharState_WalkForward)));
-                else if(vec.y() < 0.0f)
+                else
                     movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack)
                                          : (sneak ? CharState_SneakBack
                                                   : (isrunning ? CharState_RunBack : CharState_WalkBack)));
             }
-            else if(rot.z() != 0.0f)
+            else if (effectiveRotation != 0.0f)
             {
                 // Do not play turning animation for player if rotation speed is very slow.
                 // Actual threshold should take framerate in account.
@@ -2193,9 +2205,9 @@ void CharacterController::update(float duration, bool animationOnly)
                 bool isFirstPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson();
                 if (!sneak && jumpstate == JumpState_None && !isFirstPlayer && mPtr.getClass().isBipedal(mPtr))
                 {
-                    if(rot.z() > rotationThreshold)
+                    if(effectiveRotation > rotationThreshold)
                         movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight;
-                    else if(rot.z() < -rotationThreshold)
+                    else if(effectiveRotation < -rotationThreshold)
                         movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft;
                 }
             }
@@ -2317,9 +2329,9 @@ void CharacterController::update(float duration, bool animationOnly)
             world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
 
         movement = vec;
-        cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0;
+        movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0;
         if (movement.z() == 0.f)
-            cls.getMovementSettings(mPtr).mPosition[2] = 0;
+            movementSettings.mPosition[2] = 0;
         // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicSystem will actually handle it in this frame
         // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled.
 
@@ -2355,15 +2367,11 @@ void CharacterController::update(float duration, bool animationOnly)
     if(speed > 0.f)
     {
         float l = moved.length();
-
-        if((movement.x() < 0.0f && movement.x() < moved.x()*2.0f) ||
-           (movement.x() > 0.0f && movement.x() > moved.x()*2.0f))
+        if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2)
             moved.x() = movement.x();
-        if((movement.y() < 0.0f && movement.y() < moved.y()*2.0f) ||
-           (movement.y() > 0.0f && movement.y() > moved.y()*2.0f))
+        if (std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2)
             moved.y() = movement.y();
-        if((movement.z() < 0.0f && movement.z() < moved.z()*2.0f) ||
-           (movement.z() > 0.0f && movement.z() > moved.z()*2.0f))
+        if (std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2)
             moved.z() = movement.z();
         // but keep the original speed
         float newLength = moved.length();
diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp
index 0f11b8b2e4..79b8e23de4 100644
--- a/apps/openmw/mwmechanics/creaturestats.cpp
+++ b/apps/openmw/mwmechanics/creaturestats.cpp
@@ -23,7 +23,7 @@ namespace MWMechanics
           mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false),
           mHitRecovery(false), mBlock(false), mMovementFlags(0),
           mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1),
-          mDeathAnimation(-1), mTimeOfDeath(), mLevel (0)
+          mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0)
     {
         for (int i=0; i<4; ++i)
             mAiSettings[i] = 0;
diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp
index b35c1e3b6e..5e91a1b5a0 100644
--- a/apps/openmw/mwmechanics/creaturestats.hpp
+++ b/apps/openmw/mwmechanics/creaturestats.hpp
@@ -80,6 +80,9 @@ namespace MWMechanics
 
         MWWorld::TimeStamp mTimeOfDeath;
 
+        // The difference between view direction and lower body direction.
+        float mSideMovementAngle;
+
     public:
         typedef std::pair<int, std::string> SummonKey; // <ESM::MagicEffect index, spell ID>
     private:
@@ -298,6 +301,9 @@ namespace MWMechanics
         void addCorprusSpell(const std::string& sourceId, CorprusStats& stats);
 
         void removeCorprusSpell(const std::string& sourceId);
+
+        float getSideMovementAngle() const { return mSideMovementAngle; }
+        void setSideMovementAngle(float angle) { mSideMovementAngle = angle; }
     };
 }
 
diff --git a/apps/openmw/mwmechanics/movement.hpp b/apps/openmw/mwmechanics/movement.hpp
index cb9087359a..86b970e602 100644
--- a/apps/openmw/mwmechanics/movement.hpp
+++ b/apps/openmw/mwmechanics/movement.hpp
@@ -11,12 +11,14 @@ namespace MWMechanics
         float mPosition[3];
         float mRotation[3];
         float mSpeedFactor;
+        bool mIsStrafing;
 
         Movement()
         {
             mPosition[0] = mPosition[1] = mPosition[2] = 0.0f;
             mRotation[0] = mRotation[1] = mRotation[2] = 0.0f;
             mSpeedFactor = 1.f;
+            mIsStrafing = false;
         }
 
         osg::Vec3f asVec3()
diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp
index 594713a1cf..69136bac31 100644
--- a/apps/openmw/mwrender/animation.cpp
+++ b/apps/openmw/mwrender/animation.cpp
@@ -621,6 +621,8 @@ namespace MWRender
         , mTextKeyListener(nullptr)
         , mHeadYawRadians(0.f)
         , mHeadPitchRadians(0.f)
+        , mUpperBodyYawRadians(0.f)
+        , mLegsYawRadians(0.f)
         , mHasMagicEffects(false)
         , mAlpha(1.f)
     {
@@ -1334,13 +1336,36 @@ namespace MWRender
 
         updateEffects();
 
+        const float epsilon = 0.001f;
+        float yawOffset = 0;
+        if (mRootController)
+        {
+            bool enable = std::abs(mLegsYawRadians) > epsilon;
+            mRootController->setEnabled(enable);
+            if (enable)
+            {
+                mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0,0,1)));
+                yawOffset = mLegsYawRadians;
+            }
+        }
+        if (mSpineController)
+        {
+            float yaw = mUpperBodyYawRadians - yawOffset;
+            bool enable = std::abs(yaw) > epsilon;
+            mSpineController->setEnabled(enable);
+            if (enable)
+            {
+                mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0,0,1)));
+                yawOffset = mUpperBodyYawRadians;
+            }
+        }
         if (mHeadController)
         {
-            const float epsilon = 0.001f;
-            bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(mHeadYawRadians) > epsilon);
+            float yaw = mHeadYawRadians - yawOffset;
+            bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(yaw) > epsilon);
             mHeadController->setEnabled(enable);
             if (enable)
-                mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(mHeadYawRadians, osg::Vec3f(0,0,1)));
+                mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(yaw, osg::Vec3f(0,0,1)));
         }
 
         // Scripted animations should not cause movement
@@ -1801,13 +1826,17 @@ namespace MWRender
 
     void Animation::addControllers()
     {
-        mHeadController = nullptr;
+        mHeadController = addRotateController("bip01 head");
+        mSpineController = addRotateController("bip01 spine1");
+        mRootController = addRotateController("bip01");
+    }
 
-        NodeMap::const_iterator found = getNodeMap().find("bip01 head");
-        if (found == getNodeMap().end())
-            return;
-
-        osg::MatrixTransform* node = found->second;
+    RotateController* Animation::addRotateController(std::string bone)
+    {
+        auto iter = getNodeMap().find(bone);
+        if (iter == getNodeMap().end())
+            return nullptr;
+        osg::MatrixTransform* node = iter->second;
 
         bool foundKeyframeCtrl = false;
         osg::Callback* cb = node->getUpdateCallback();
@@ -1820,13 +1849,15 @@ namespace MWRender
             }
             cb = cb->getNestedCallback();
         }
-
+        // Without KeyframeController the orientation will not be reseted each frame, so
+        // RotateController shouldn't be used for such nodes.
         if (!foundKeyframeCtrl)
-            return;
+            return nullptr;
 
-        mHeadController = new RotateController(mObjectRoot.get());
-        node->addUpdateCallback(mHeadController);
-        mActiveControllers.emplace_back(node, mHeadController);
+        RotateController* controller = new RotateController(mObjectRoot.get());
+        node->addUpdateCallback(controller);
+        mActiveControllers.emplace_back(node, controller);
+        return controller;
     }
 
     void Animation::setHeadPitch(float pitchRadians)
diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp
index c53cf98a95..564952a905 100644
--- a/apps/openmw/mwrender/animation.hpp
+++ b/apps/openmw/mwrender/animation.hpp
@@ -267,8 +267,15 @@ protected:
     TextKeyListener* mTextKeyListener;
 
     osg::ref_ptr<RotateController> mHeadController;
+    osg::ref_ptr<RotateController> mSpineController;
+    osg::ref_ptr<RotateController> mRootController;
     float mHeadYawRadians;
     float mHeadPitchRadians;
+    float mUpperBodyYawRadians;
+    float mLegsYawRadians;
+
+    RotateController* addRotateController(std::string bone);
+
     bool mHasMagicEffects;
 
     osg::ref_ptr<SceneUtil::LightSource> mGlowLight;
@@ -477,6 +484,12 @@ public:
     virtual void setHeadYaw(float yawRadians);
     virtual float getHeadPitch() const;
     virtual float getHeadYaw() const;
+
+    virtual void setUpperBodyYawRadians(float v) { mUpperBodyYawRadians = v; }
+    virtual void setLegsYawRadians(float v) { mLegsYawRadians = v; }
+    virtual float getUpperBodyYawRadians() const { return mUpperBodyYawRadians; }
+    virtual float getLegsYawRadians() const { return mLegsYawRadians; }
+
     virtual void setAccurateAiming(bool enabled) {}
     virtual bool canBeHarvested() const { return false; }
 
diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp
index f5d63343bc..251f7f5939 100644
--- a/apps/openmw/mwrender/camera.cpp
+++ b/apps/openmw/mwrender/camera.cpp
@@ -244,6 +244,9 @@ namespace MWRender
         else
             mViewModeToggleQueued = false;
 
+        if (mTrackingPtr.getClass().isActor())
+            mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).setSideMovementAngle(0);
+
         mFirstPersonView = !mFirstPersonView;
         processViewChange();
     }
diff --git a/files/settings-default.cfg b/files/settings-default.cfg
index b42924de0c..3a7c9a8b48 100644
--- a/files/settings-default.cfg
+++ b/files/settings-default.cfg
@@ -302,6 +302,12 @@ projectiles enchant multiplier = 0
 # This means that unlike Morrowind you will be able to knock down actors using this effect.
 uncapped damage fatigue = false
 
+# Turn lower body to movement direction. 'true' makes diagonal movement more realistic.
+turn to movement direction = false
+
+# Turning speed multiplier. Makes difference only if 'turn to movement direction' is enabled.
+turn to movement direction speed coef = 1.0
+
 [General]
 
 # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).