mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-19 22:53:50 +00:00
Merge branch 'master' into 'rotation'
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
ad1021721f
17 changed files with 413 additions and 87 deletions
|
@ -49,6 +49,7 @@
|
|||
Bug #5557: Diagonal movement is noticeably slower with analogue stick
|
||||
Feature #390: 3rd person look "over the shoulder"
|
||||
Feature #2386: Distant Statics in the form of Object Paging
|
||||
Feature #4894: Consider actors as obstacles for pathfinding
|
||||
Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher
|
||||
Feature #5362: Show the soul gems' trapped soul in count dialog
|
||||
Feature #5445: Handle NiLines
|
||||
|
@ -59,6 +60,7 @@
|
|||
Feature #5525: Search fields tweaks (utf-8)
|
||||
Feature #5545: Option to allow stealing from an unconscious NPC during combat
|
||||
Feature #5579: MCP SetAngle enhancement
|
||||
Feature #5610: Actors movement should be smoother
|
||||
Task #5480: Drop Qt4 support
|
||||
Task #5520: Improve cell name autocompleter implementation
|
||||
|
||||
|
|
|
@ -88,6 +88,7 @@ bool Launcher::AdvancedPage::loadSettings()
|
|||
loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game");
|
||||
loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game");
|
||||
loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game");
|
||||
loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game");
|
||||
int unarmedFactorsStrengthIndex = mEngineSettings.getInt("strength influences hand to hand", "Game");
|
||||
if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2)
|
||||
unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex);
|
||||
|
@ -112,6 +113,7 @@ bool Launcher::AdvancedPage::loadSettings()
|
|||
loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game");
|
||||
}
|
||||
loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game");
|
||||
loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game");
|
||||
|
||||
const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain");
|
||||
const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain");
|
||||
|
@ -200,6 +202,7 @@ void Launcher::AdvancedPage::saveSettings()
|
|||
saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game");
|
||||
saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game");
|
||||
saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game");
|
||||
saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game");
|
||||
int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex();
|
||||
if (unarmedFactorsStrengthIndex != mEngineSettings.getInt("strength influences hand to hand", "Game"))
|
||||
mEngineSettings.setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex);
|
||||
|
@ -220,6 +223,7 @@ void Launcher::AdvancedPage::saveSettings()
|
|||
saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game");
|
||||
saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game");
|
||||
saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game");
|
||||
saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game");
|
||||
|
||||
const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain");
|
||||
const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain");
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <components/sceneutil/positionattitudetransform.hpp>
|
||||
#include <components/debug/debuglog.hpp>
|
||||
#include <components/misc/rng.hpp>
|
||||
#include <components/misc/mathutil.hpp>
|
||||
#include <components/settings/settings.hpp>
|
||||
|
||||
#include "../mwworld/esmstore.hpp"
|
||||
|
@ -36,6 +37,7 @@
|
|||
#include "aicombataction.hpp"
|
||||
#include "aifollow.hpp"
|
||||
#include "aipursue.hpp"
|
||||
#include "aiwander.hpp"
|
||||
#include "actor.hpp"
|
||||
#include "summoning.hpp"
|
||||
#include "combat.hpp"
|
||||
|
@ -424,7 +426,7 @@ namespace MWMechanics
|
|||
const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3());
|
||||
float sqrDist = (actor1Pos - actor2Pos).length2();
|
||||
|
||||
if (sqrDist > maxDistance*maxDistance)
|
||||
if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance))
|
||||
return;
|
||||
|
||||
// stop tracking when target is behind the actor
|
||||
|
@ -432,10 +434,7 @@ namespace MWMechanics
|
|||
osg::Vec3f targetDirection(actor2Pos - actor1Pos);
|
||||
actorDirection.z() = 0;
|
||||
targetDirection.z() = 0;
|
||||
actorDirection.normalize();
|
||||
targetDirection.normalize();
|
||||
if (std::acos(actorDirection * targetDirection) < osg::DegreesToRadians(90.f)
|
||||
&& sqrDist <= sqrHeadTrackDistance
|
||||
if (actorDirection * targetDirection > 0
|
||||
&& MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function
|
||||
&& MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor))
|
||||
{
|
||||
|
@ -473,6 +472,9 @@ namespace MWMechanics
|
|||
|
||||
void Actors::updateMovementSpeed(const MWWorld::Ptr& actor)
|
||||
{
|
||||
if (mSmoothMovement)
|
||||
return;
|
||||
|
||||
CreatureStats &stats = actor.getClass().getCreatureStats(actor);
|
||||
MWMechanics::AiSequence& seq = stats.getAiSequence();
|
||||
|
||||
|
@ -481,9 +483,10 @@ namespace MWMechanics
|
|||
osg::Vec3f targetPos = seq.getActivePackage().getDestination();
|
||||
osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
|
||||
float distance = (targetPos - actorPos).length();
|
||||
|
||||
if (distance < DECELERATE_DISTANCE)
|
||||
{
|
||||
float speedCoef = std::max(0.7f, 0.1f * (distance/64.f + 2.f));
|
||||
float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE);
|
||||
auto& movement = actor.getClass().getMovementSettings(actor);
|
||||
movement.mPosition[0] *= speedCoef;
|
||||
movement.mPosition[1] *= speedCoef;
|
||||
|
@ -587,7 +590,10 @@ namespace MWMechanics
|
|||
|
||||
if (!actorState.isTurningToPlayer())
|
||||
{
|
||||
actorState.setAngleToPlayer(std::atan2(dir.x(), dir.y()));
|
||||
float angle = std::atan2(dir.x(), dir.y());
|
||||
actorState.setAngleToPlayer(angle);
|
||||
float deltaAngle = Misc::normalizeAngle(angle - actor.getRefData().getPosition().rot[2]);
|
||||
if (!mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f))
|
||||
actorState.setTurningToPlayer(true);
|
||||
}
|
||||
}
|
||||
|
@ -1460,7 +1466,7 @@ namespace MWMechanics
|
|||
}
|
||||
}
|
||||
|
||||
Actors::Actors()
|
||||
Actors::Actors() : mSmoothMovement(Settings::Manager::getBool("smooth movement", "Game"))
|
||||
{
|
||||
mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning
|
||||
|
||||
|
@ -1659,6 +1665,131 @@ namespace MWMechanics
|
|||
|
||||
}
|
||||
|
||||
void Actors::predictAndAvoidCollisions()
|
||||
{
|
||||
const float minGap = 10.f;
|
||||
const float maxDistToCheck = 100.f;
|
||||
const float maxTimeToCheck = 1.f;
|
||||
static const bool giveWayWhenIdle = Settings::Manager::getBool("NPCs give way", "Game");
|
||||
|
||||
MWWorld::Ptr player = getPlayer();
|
||||
MWBase::World* world = MWBase::Environment::get().getWorld();
|
||||
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = iter->first;
|
||||
if (ptr == player)
|
||||
continue; // Don't interfere with player controls.
|
||||
|
||||
Movement& movement = ptr.getClass().getMovementSettings(ptr);
|
||||
osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]);
|
||||
bool isMoving = origMovement.length2() > 0.01;
|
||||
|
||||
// Moving NPCs always should avoid collisions.
|
||||
// Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either
|
||||
// follow player or have a AIWander package with non-empty wander area.
|
||||
bool shouldAvoidCollision = isMoving;
|
||||
bool shouldTurnToApproachingActor = !isMoving;
|
||||
MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets).
|
||||
for (const auto& package : ptr.getClass().getCreatureStats(ptr).getAiSequence())
|
||||
{
|
||||
if (package->getTypeId() == AiPackageTypeId::Follow)
|
||||
shouldAvoidCollision = true;
|
||||
else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle)
|
||||
{
|
||||
if (!dynamic_cast<const AiWander*>(package.get())->isStationary())
|
||||
shouldAvoidCollision = true;
|
||||
}
|
||||
else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue)
|
||||
{
|
||||
currentTarget = package->getTarget();
|
||||
shouldAvoidCollision = isMoving;
|
||||
shouldTurnToApproachingActor = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!shouldAvoidCollision)
|
||||
continue;
|
||||
|
||||
float maxSpeed = ptr.getClass().getMaxSpeed(ptr);
|
||||
osg::Vec2f baseSpeed = origMovement * maxSpeed;
|
||||
osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3();
|
||||
float baseRotZ = ptr.getRefData().getPosition().rot[2];
|
||||
osg::Vec3f halfExtents = world->getHalfExtents(ptr);
|
||||
|
||||
float timeToCollision = maxTimeToCheck;
|
||||
osg::Vec2f movementCorrection(0, 0);
|
||||
float angleToApproachingActor = 0;
|
||||
|
||||
// Iterate through all other actors and predict collisions.
|
||||
for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter)
|
||||
{
|
||||
const MWWorld::Ptr& otherPtr = otherIter->first;
|
||||
if (otherPtr == ptr || otherPtr == currentTarget)
|
||||
continue;
|
||||
|
||||
osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr);
|
||||
osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos;
|
||||
osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ);
|
||||
|
||||
// Ignore actors which are not close enough or come from behind.
|
||||
if (deltaPos.length2() > maxDistToCheck * maxDistToCheck || relPos.y() < 0)
|
||||
continue;
|
||||
|
||||
// Don't check for a collision if vertical distance is greater then the actor's height.
|
||||
if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2)
|
||||
continue;
|
||||
|
||||
osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() *
|
||||
otherPtr.getClass().getMaxSpeed(otherPtr);
|
||||
float rotZ = otherPtr.getRefData().getPosition().rot[2];
|
||||
osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed;
|
||||
|
||||
float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x();
|
||||
collisionDist = std::min(collisionDist, relPos.length());
|
||||
|
||||
// Find the earliest `t` when |relPos + relSpeed * t| == collisionDist.
|
||||
float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y();
|
||||
float v2 = relSpeed.length2();
|
||||
float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist);
|
||||
if (Dh <= 0 || v2 == 0)
|
||||
continue; // No solution; distance is always >= collisionDist.
|
||||
float t = (-vr - std::sqrt(Dh)) / v2;
|
||||
|
||||
if (t < 0 || t > timeToCollision)
|
||||
continue;
|
||||
|
||||
// Check visibility and awareness last as it's expensive.
|
||||
if (!MWBase::Environment::get().getWorld()->getLOS(otherPtr, ptr))
|
||||
continue;
|
||||
if (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(otherPtr, ptr))
|
||||
continue;
|
||||
|
||||
timeToCollision = t;
|
||||
angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y());
|
||||
osg::Vec2f posAtT = relPos + relSpeed * t;
|
||||
float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * maxSpeed);
|
||||
movementCorrection = posAtT * coef;
|
||||
// Step to the side rather than backward. Otherwise player will be able to push the NPC far away from it's original location.
|
||||
movementCorrection.y() = std::max(0.f, movementCorrection.y());
|
||||
}
|
||||
|
||||
if (timeToCollision < maxTimeToCheck)
|
||||
{
|
||||
// Try to evade the nearest collision.
|
||||
osg::Vec2f newMovement = origMovement + movementCorrection;
|
||||
if (isMoving)
|
||||
{ // Keep the original speed.
|
||||
newMovement.normalize();
|
||||
newMovement *= origMovement.length();
|
||||
}
|
||||
movement.mPosition[0] = newMovement.x();
|
||||
movement.mPosition[1] = newMovement.y();
|
||||
if (shouldTurnToApproachingActor)
|
||||
zTurn(ptr, angleToApproachingActor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Actors::update (float duration, bool paused)
|
||||
{
|
||||
if(!paused)
|
||||
|
@ -1769,14 +1900,12 @@ namespace MWMechanics
|
|||
|
||||
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
|
||||
bool firstPersonPlayer = isPlayer && world->isFirstPerson();
|
||||
bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue);
|
||||
|
||||
// 1. Unconsious actor can not track target
|
||||
// 2. Actors in combat and pursue mode do not bother to headtrack
|
||||
// 3. Player character does not use headtracking in the 1st-person view
|
||||
if (!stats.getKnockedDown() &&
|
||||
!stats.getAiSequence().isInCombat() &&
|
||||
!stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue) &&
|
||||
!firstPersonPlayer)
|
||||
if (!stats.getKnockedDown() && !firstPersonPlayer && !inCombatOrPursue)
|
||||
{
|
||||
for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it)
|
||||
{
|
||||
|
@ -1786,6 +1915,17 @@ namespace MWMechanics
|
|||
}
|
||||
}
|
||||
|
||||
if (!stats.getKnockedDown() && !isPlayer && inCombatOrPursue)
|
||||
{
|
||||
// Actors in combat and pursue mode always look at their target.
|
||||
for (const auto& package : stats.getAiSequence())
|
||||
{
|
||||
headTrackTarget = package->getTarget();
|
||||
if (!headTrackTarget.isEmpty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ctrl->setHeadTrackTarget(headTrackTarget);
|
||||
}
|
||||
|
||||
|
@ -1824,6 +1964,10 @@ namespace MWMechanics
|
|||
}
|
||||
}
|
||||
|
||||
static const bool avoidCollisions = Settings::Manager::getBool("NPCs avoid collisions", "Game");
|
||||
if (avoidCollisions)
|
||||
predictAndAvoidCollisions();
|
||||
|
||||
timerUpdateAITargets += duration;
|
||||
timerUpdateHeadTrack += duration;
|
||||
timerUpdateEquippedLight += duration;
|
||||
|
|
|
@ -63,6 +63,8 @@ namespace MWMechanics
|
|||
|
||||
void purgeSpellEffects (int casterActorId);
|
||||
|
||||
void predictAndAvoidCollisions();
|
||||
|
||||
public:
|
||||
|
||||
Actors();
|
||||
|
@ -209,6 +211,7 @@ namespace MWMechanics
|
|||
float mTimerDisposeSummonsCorpses;
|
||||
float mActorsProcessingRange;
|
||||
|
||||
bool mSmoothMovement;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterContro
|
|||
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
|
||||
|
||||
actorClass.getMovementSettings(actor).mPosition[1] = 1;
|
||||
smoothTurn(actor, -180, 0);
|
||||
smoothTurn(actor, -osg::PI / 2, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
#include <components/esm/aisequence.hpp>
|
||||
|
||||
#include <components/misc/mathutil.hpp>
|
||||
|
||||
#include <components/sceneutil/positionattitudetransform.hpp>
|
||||
|
||||
#include "../mwphysics/collisiontype.hpp"
|
||||
|
@ -240,10 +242,6 @@ namespace MWMechanics
|
|||
|
||||
if (storage.mReadyToAttack)
|
||||
{
|
||||
storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
|
||||
// start new attack
|
||||
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);
|
||||
|
||||
if (isRangedCombat)
|
||||
{
|
||||
// rotate actor taking into account target movement direction and projectile speed
|
||||
|
@ -259,6 +257,10 @@ namespace MWMechanics
|
|||
storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
|
||||
storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated
|
||||
}
|
||||
|
||||
storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
|
||||
// start new attack
|
||||
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -372,9 +374,13 @@ namespace MWMechanics
|
|||
void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage)
|
||||
{
|
||||
// apply combat movement
|
||||
float deltaAngle = storage.mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2];
|
||||
osg::Vec2f movement = Misc::rotateVec2f(
|
||||
osg::Vec2f(storage.mMovement.mPosition[0], storage.mMovement.mPosition[1]), -deltaAngle);
|
||||
|
||||
MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
|
||||
actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0];
|
||||
actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1];
|
||||
actorMovementSettings.mPosition[0] = movement.x();
|
||||
actorMovementSettings.mPosition[1] = movement.y();
|
||||
actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];
|
||||
|
||||
rotateActorOnAxis(actor, 2, actorMovementSettings, storage);
|
||||
|
@ -385,26 +391,11 @@ namespace MWMechanics
|
|||
MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage)
|
||||
{
|
||||
actorMovementSettings.mRotation[axis] = 0;
|
||||
float& targetAngleRadians = storage.mMovement.mRotation[axis];
|
||||
if (targetAngleRadians != 0)
|
||||
{
|
||||
// Some attack animations contain small amount of movement.
|
||||
// Since we use cone shapes for melee, we can use a threshold to avoid jittering
|
||||
std::shared_ptr<Action>& currentAction = storage.mCurrentAction;
|
||||
bool isRangedCombat = false;
|
||||
currentAction->getCombatRange(isRangedCombat);
|
||||
// Check if the actor now facing desired direction, no need to turn any more
|
||||
if (isRangedCombat)
|
||||
{
|
||||
if (smoothTurn(actor, targetAngleRadians, axis))
|
||||
targetAngleRadians = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (smoothTurn(actor, targetAngleRadians, axis, osg::DegreesToRadians(3.f)))
|
||||
targetAngleRadians = 0;
|
||||
}
|
||||
}
|
||||
storage.mCurrentAction->getCombatRange(isRangedCombat);
|
||||
float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f);
|
||||
float targetAngleRadians = storage.mMovement.mRotation[axis];
|
||||
smoothTurn(actor, targetAngleRadians, axis, eps);
|
||||
}
|
||||
|
||||
MWWorld::Ptr AiCombat::getTarget() const
|
||||
|
@ -489,12 +480,19 @@ namespace MWMechanics
|
|||
// Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff
|
||||
else if (actor.getClass().isBipedal(actor))
|
||||
{
|
||||
// apply sideway movement (kind of dodging) with some probability
|
||||
// if actor is within range of target's weapon
|
||||
if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
|
||||
float moveDuration = 0;
|
||||
float angleToTarget = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]);
|
||||
// Apply a big side step if enemy tries to get around and come from behind.
|
||||
// Otherwise apply a random side step (kind of dodging) with some probability
|
||||
// if actor is within range of target's weapon.
|
||||
if (std::abs(angleToTarget) > osg::PI / 4)
|
||||
moveDuration = 0.2;
|
||||
else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
|
||||
moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
|
||||
if (moveDuration > 0)
|
||||
{
|
||||
mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right
|
||||
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
|
||||
mTimerCombatMove = moveDuration;
|
||||
mCombatMove = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <components/esm/loadmgef.hpp>
|
||||
#include <components/detournavigator/navigator.hpp>
|
||||
#include <components/misc/coordinateconverter.hpp>
|
||||
#include <components/settings/settings.hpp>
|
||||
|
||||
#include "../mwbase/world.hpp"
|
||||
#include "../mwbase/environment.hpp"
|
||||
|
@ -87,6 +88,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
|
|||
//... But AI processing distance may increase in the future.
|
||||
if (isNearInactiveCell(position))
|
||||
{
|
||||
actor.getClass().getMovementSettings(actor).mPosition[0] = 0;
|
||||
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
|
||||
world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest);
|
||||
return false;
|
||||
|
@ -169,12 +171,34 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
|
|||
}
|
||||
|
||||
// turn to next path point by X,Z axes
|
||||
zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y()));
|
||||
float zAngleToNext = mPathFinder.getZAngleToNext(position.x(), position.y());
|
||||
zTurn(actor, zAngleToNext);
|
||||
smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0);
|
||||
|
||||
const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front();
|
||||
mObstacleCheck.update(actor, destination, duration);
|
||||
|
||||
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
|
||||
if (smoothMovement)
|
||||
{
|
||||
const float smoothTurnReservedDist = 150;
|
||||
auto& movement = actor.getClass().getMovementSettings(actor);
|
||||
float distToNextSqr = osg::Vec2f(destination.x() - position.x(), destination.y() - position.y()).length2();
|
||||
float diffAngle = zAngleToNext - actor.getRefData().getPosition().rot[2];
|
||||
if (std::cos(diffAngle) < -0.1)
|
||||
movement.mPosition[0] = movement.mPosition[1] = 0;
|
||||
else if (distToNextSqr > smoothTurnReservedDist * smoothTurnReservedDist)
|
||||
{ // Go forward (and slowly turn towards the next path point)
|
||||
movement.mPosition[0] = 0;
|
||||
movement.mPosition[1] = 1;
|
||||
}
|
||||
else
|
||||
{ // Next path point is near, so use diagonal movement to follow the path precisely.
|
||||
movement.mPosition[0] = std::sin(diffAngle);
|
||||
movement.mPosition[1] = std::max(std::cos(diffAngle), 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
// handle obstacles on the way
|
||||
evadeObstacles(actor);
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
namespace MWMechanics
|
||||
{
|
||||
static const int COUNT_BEFORE_RESET = 10;
|
||||
static const float DOOR_CHECK_INTERVAL = 1.5f;
|
||||
static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f;
|
||||
|
||||
// to prevent overcrowding
|
||||
static const int DESTINATION_TOLERANCE = 64;
|
||||
|
@ -96,6 +96,7 @@ namespace MWMechanics
|
|||
|
||||
void stopMovement(const MWWorld::Ptr& actor)
|
||||
{
|
||||
actor.getClass().getMovementSettings(actor).mPosition[0] = 0;
|
||||
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
|
||||
}
|
||||
|
||||
|
@ -424,15 +425,14 @@ namespace MWMechanics
|
|||
|
||||
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
||||
{
|
||||
// Check if an idle actor is too close to a door - if so start walking
|
||||
storage.mDoorCheckDuration += duration;
|
||||
// Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking.
|
||||
storage.mCheckIdlePositionTimer += duration;
|
||||
|
||||
if (storage.mDoorCheckDuration >= DOOR_CHECK_INTERVAL)
|
||||
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary())
|
||||
{
|
||||
storage.mDoorCheckDuration = 0; // restart timer
|
||||
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
|
||||
if (mDistance && // actor is not intended to be stationary
|
||||
proximityToDoor(actor, distance*1.6f))
|
||||
storage.mCheckIdlePositionTimer = 0; // restart timer
|
||||
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f;
|
||||
if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance))
|
||||
{
|
||||
storage.setState(AiWanderStorage::Wander_MoveNow);
|
||||
storage.mTrimCurrentNode = false; // just in case
|
||||
|
@ -451,6 +451,20 @@ namespace MWMechanics
|
|||
}
|
||||
}
|
||||
|
||||
bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const
|
||||
{
|
||||
const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
|
||||
auto cell = actor.getCell()->getCell();
|
||||
for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes)
|
||||
{
|
||||
osg::Vec3f point(node.mX, node.mY, node.mZ);
|
||||
Misc::CoordinateConverter(cell).toWorld(point);
|
||||
if ((actorPos - point).length2() < distance * distance)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
||||
{
|
||||
// Is there no destination or are we there yet?
|
||||
|
@ -468,6 +482,9 @@ namespace MWMechanics
|
|||
|
||||
void AiWander::onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage)
|
||||
{
|
||||
// Wait while fully stop before starting idle animation (important if "smooth movement" is enabled).
|
||||
if (actor.getClass().getCurrentSpeed(actor) > 0)
|
||||
return;
|
||||
|
||||
unsigned short idleAnimation = getRandomIdle();
|
||||
storage.mIdleAnimation = idleAnimation;
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace MWMechanics
|
|||
ESM::Pathgrid::Point mCurrentNode;
|
||||
bool mTrimCurrentNode;
|
||||
|
||||
float mDoorCheckDuration;
|
||||
float mCheckIdlePositionTimer;
|
||||
int mStuckCount;
|
||||
|
||||
AiWanderStorage():
|
||||
|
@ -66,7 +66,7 @@ namespace MWMechanics
|
|||
mPopulateAvailableNodes(true),
|
||||
mAllowedNodes(),
|
||||
mTrimCurrentNode(false),
|
||||
mDoorCheckDuration(0), // TODO: maybe no longer needed
|
||||
mCheckIdlePositionTimer(0),
|
||||
mStuckCount(0)
|
||||
{};
|
||||
|
||||
|
@ -117,6 +117,8 @@ namespace MWMechanics
|
|||
return mDestination;
|
||||
}
|
||||
|
||||
bool isStationary() const { return mDistance == 0; }
|
||||
|
||||
private:
|
||||
void stopWalking(const MWWorld::Ptr& actor);
|
||||
|
||||
|
@ -137,6 +139,7 @@ namespace MWMechanics
|
|||
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
|
||||
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
|
||||
void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage);
|
||||
bool isNearAllowedNode(const MWWorld::Ptr &actor, const AiWanderStorage& storage, float distance) const;
|
||||
|
||||
const int mDistance; // how far the actor can wander from the spawn point
|
||||
const int mDuration;
|
||||
|
|
|
@ -1963,6 +1963,50 @@ void CharacterController::update(float duration, bool animationOnly)
|
|||
if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f)
|
||||
movementSettings.mSpeedFactor *= 2.f;
|
||||
|
||||
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
|
||||
if (smoothMovement && !isFirstPersonPlayer)
|
||||
{
|
||||
float angle = mPtr.getRefData().getPosition().rot[2];
|
||||
osg::Vec2f targetSpeed = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor;
|
||||
osg::Vec2f delta = targetSpeed - mSmoothedSpeed;
|
||||
float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length();
|
||||
float deltaLen = delta.length();
|
||||
|
||||
float maxDelta;
|
||||
if (std::abs(speedDelta) < deltaLen / 2)
|
||||
// Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point).
|
||||
maxDelta = duration * (isPlayer ? 3.f : 6.f);
|
||||
else if (isPlayer && speedDelta < -deltaLen / 2)
|
||||
// As soon as controls are released, mwinput switches player from running to walking.
|
||||
// So stopping should be instant for player, otherwise it causes a small twitch.
|
||||
maxDelta = 1;
|
||||
else // In all other cases speeding up and stopping are smooth.
|
||||
maxDelta = duration * 3.f;
|
||||
|
||||
if (deltaLen > maxDelta)
|
||||
delta *= maxDelta / deltaLen;
|
||||
mSmoothedSpeed += delta;
|
||||
|
||||
osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle);
|
||||
movementSettings.mSpeedFactor = newSpeed.normalize();
|
||||
vec.x() = newSpeed.x();
|
||||
vec.y() = newSpeed.y();
|
||||
|
||||
const float eps = 0.001f;
|
||||
if (movementSettings.mSpeedFactor < eps)
|
||||
{
|
||||
movementSettings.mSpeedFactor = 0;
|
||||
vec.x() = 0;
|
||||
vec.y() = 1;
|
||||
}
|
||||
else if ((vec.y() < 0) != mIsMovingBackward)
|
||||
{
|
||||
if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward)
|
||||
vec.y() = mIsMovingBackward ? -eps : eps;
|
||||
}
|
||||
vec.normalize();
|
||||
}
|
||||
|
||||
float effectiveRotation = rot.z();
|
||||
bool canMove = cls.getMaxSpeed(mPtr) > 0;
|
||||
static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game");
|
||||
|
@ -1994,6 +2038,8 @@ void CharacterController::update(float duration, bool animationOnly)
|
|||
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2);
|
||||
else
|
||||
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4);
|
||||
if (smoothMovement && !isPlayer && !inwater)
|
||||
mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2);
|
||||
|
||||
speed = cls.getCurrentSpeed(mPtr);
|
||||
vec.x() *= speed;
|
||||
|
@ -2185,13 +2231,11 @@ void CharacterController::update(float duration, bool animationOnly)
|
|||
: (sneak ? CharState_SneakBack
|
||||
: (isrunning ? CharState_RunBack : CharState_WalkBack)));
|
||||
}
|
||||
else if (effectiveRotation != 0.0f)
|
||||
else
|
||||
{
|
||||
// Do not play turning animation for player if rotation speed is very slow.
|
||||
// Actual threshold should take framerate in account.
|
||||
float rotationThreshold = 0.f;
|
||||
if (isPlayer)
|
||||
rotationThreshold = 0.015 * 60 * duration;
|
||||
float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration;
|
||||
|
||||
// It seems only bipedal actors use turning animations.
|
||||
// Also do not use turning animations in the first-person view and when sneaking.
|
||||
|
@ -2695,10 +2739,9 @@ void CharacterController::setVisibility(float visibility)
|
|||
void CharacterController::setAttackTypeBasedOnMovement()
|
||||
{
|
||||
float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition;
|
||||
|
||||
if (move[1] && !move[0]) // forward-backward
|
||||
if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward
|
||||
mAttackType = "thrust";
|
||||
else if (move[0] && !move[1]) //sideway
|
||||
else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway
|
||||
mAttackType = "slash";
|
||||
else
|
||||
mAttackType = "chop";
|
||||
|
@ -2893,19 +2936,21 @@ void CharacterController::updateHeadTracking(float duration)
|
|||
return;
|
||||
const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0);
|
||||
|
||||
zAngleRadians = std::atan2(direction.x(), direction.y()) - std::atan2(actorDirection.x(), actorDirection.y());
|
||||
xAngleRadians = -std::asin(direction.z());
|
||||
zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y());
|
||||
xAngleRadians = std::asin(direction.z());
|
||||
}
|
||||
|
||||
const double xLimit = osg::DegreesToRadians(40.0);
|
||||
const double zLimit = osg::DegreesToRadians(30.0);
|
||||
zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians), -xLimit, xLimit);
|
||||
xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -zLimit, zLimit);
|
||||
}
|
||||
double zLimitOffset = mAnimation->getUpperBodyYawRadians();
|
||||
xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -xLimit, xLimit);
|
||||
zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians),
|
||||
-zLimit + zLimitOffset, zLimit + zLimitOffset);
|
||||
|
||||
float factor = duration*5;
|
||||
factor = std::min(factor, 1.f);
|
||||
xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngleRadians);
|
||||
zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngleRadians);
|
||||
xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians;
|
||||
zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians;
|
||||
|
||||
mAnimation->setHeadPitch(xAngleRadians);
|
||||
mAnimation->setHeadYaw(zAngleRadians);
|
||||
|
|
|
@ -196,6 +196,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener
|
|||
float mTimeUntilWake;
|
||||
|
||||
bool mIsMovingBackward;
|
||||
osg::Vec2f mSmoothedSpeed;
|
||||
|
||||
void setAttackTypeBasedOnMovement();
|
||||
|
||||
|
|
|
@ -88,6 +88,24 @@ namespace
|
|||
const auto halfExtents = world->getHalfExtents(actor);
|
||||
return 2.0 * halfExtents.z();
|
||||
}
|
||||
|
||||
// Returns true if turn in `p2` is less than 10 degrees and all the 3 points are almost on one line.
|
||||
bool isAlmostStraight(const osg::Vec3f& p1, const osg::Vec3f& p2, const osg::Vec3f& p3, float pointTolerance) {
|
||||
osg::Vec3f v1 = p1 - p2;
|
||||
osg::Vec3f v3 = p3 - p2;
|
||||
v1.z() = v3.z() = 0;
|
||||
float dotProduct = v1.x() * v3.x() + v1.y() * v3.y();
|
||||
float crossProduct = v1.x() * v3.y() - v1.y() * v3.x();
|
||||
|
||||
// Check that the angle between v1 and v3 is less or equal than 10 degrees.
|
||||
static const float cos170 = std::cos(osg::PI / 180 * 170);
|
||||
bool checkAngle = dotProduct <= cos170 * v1.length() * v3.length();
|
||||
|
||||
// Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`.
|
||||
bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length() * 2;
|
||||
|
||||
return checkAngle && checkDist;
|
||||
}
|
||||
}
|
||||
|
||||
namespace MWMechanics
|
||||
|
@ -286,6 +304,11 @@ namespace MWMechanics
|
|||
while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance)
|
||||
mPath.pop_front();
|
||||
|
||||
while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance))
|
||||
mPath.erase(mPath.begin() + 1);
|
||||
if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance))
|
||||
mPath.pop_front();
|
||||
|
||||
if (mPath.size() == 1 && sqrDistanceIgnoreZ(mPath.front(), position) < destinationTolerance * destinationTolerance)
|
||||
mPath.pop_front();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#include "steering.hpp"
|
||||
|
||||
#include <components/misc/mathutil.hpp>
|
||||
#include <components/settings/settings.hpp>
|
||||
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwworld/ptr.hpp"
|
||||
|
||||
|
@ -12,19 +15,8 @@ namespace MWMechanics
|
|||
|
||||
bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians)
|
||||
{
|
||||
float currentAngle (actor.getRefData().getPosition().rot[axis]);
|
||||
float diff (targetAngleRadians - currentAngle);
|
||||
if (std::abs(diff) >= osg::DegreesToRadians(180.f))
|
||||
{
|
||||
if (diff >= 0)
|
||||
{
|
||||
diff = diff - osg::DegreesToRadians(360.f);
|
||||
}
|
||||
else
|
||||
{
|
||||
diff = osg::DegreesToRadians(360.f) + diff;
|
||||
}
|
||||
}
|
||||
MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
|
||||
float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]);
|
||||
float absDiff = std::abs(diff);
|
||||
|
||||
// The turning animation actually moves you slightly, so the angle will be wrong again.
|
||||
|
@ -33,10 +25,14 @@ bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, f
|
|||
return true;
|
||||
|
||||
float limit = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration();
|
||||
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
|
||||
if (smoothMovement)
|
||||
limit *= std::min(absDiff / osg::PI + 0.1, 0.5);
|
||||
|
||||
if (absDiff > limit)
|
||||
diff = osg::sign(diff) * limit;
|
||||
|
||||
actor.getClass().getMovementSettings(actor).mRotation[axis] = diff;
|
||||
movement.mRotation[axis] = diff;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1507,6 +1507,8 @@ namespace NifOsg
|
|||
|
||||
texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);
|
||||
texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);
|
||||
|
||||
uvSet = tex.uvSet;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -331,8 +331,43 @@ If enabled then the character turns lower body to the direction of movement. Upp
|
|||
|
||||
This setting can be controlled in Advanced tab of the launcher.
|
||||
|
||||
smooth movement
|
||||
---------------
|
||||
|
||||
:Type: boolean
|
||||
:Range: True/False
|
||||
:Default: False
|
||||
|
||||
Makes NPCs and player movement more smooth.
|
||||
|
||||
Recommended to use with "turn to movement direction" enabled.
|
||||
|
||||
This setting can be controlled in Advanced tab of the launcher.
|
||||
|
||||
NPCs avoid collisions
|
||||
---------------------
|
||||
|
||||
:Type: boolean
|
||||
:Range: True/False
|
||||
:Default: False
|
||||
|
||||
If enabled NPCs apply evasion maneuver to avoid collisions with others.
|
||||
|
||||
This setting can be controlled in Advanced tab of the launcher.
|
||||
|
||||
NPCs give way
|
||||
-------------
|
||||
|
||||
:Type: boolean
|
||||
:Range: True/False
|
||||
:Default: True
|
||||
|
||||
Standing NPCs give way to moving ones. Works only if 'NPCs avoid collisions' is enabled.
|
||||
|
||||
This setting can only be configured by editing the settings configuration file.
|
||||
|
||||
swim upward correction
|
||||
----------------
|
||||
----------------------
|
||||
|
||||
:Type: boolean
|
||||
:Range: True/False
|
||||
|
|
|
@ -325,6 +325,15 @@ uncapped damage fatigue = false
|
|||
# Turn lower body to movement direction. 'true' makes diagonal movement more realistic.
|
||||
turn to movement direction = false
|
||||
|
||||
# Makes all movements of NPCs and player more smooth.
|
||||
smooth movement = false
|
||||
|
||||
# All actors avoid collisions with other actors.
|
||||
NPCs avoid collisions = false
|
||||
|
||||
# Give way to moving actors when idle. Requires 'NPCs avoid collisions' to be enabled.
|
||||
NPCs give way = true
|
||||
|
||||
# Makes player swim a bit upward from the line of sight.
|
||||
swim upward correction = false
|
||||
|
||||
|
|
|
@ -43,6 +43,16 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="avoidCollisionsCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>NPCs avoid collisions</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="enableNavigatorCheckBox">
|
||||
<property name="toolTip">
|
||||
|
@ -233,6 +243,16 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="smoothMovementCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Smooth movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="distantLandCheckBox">
|
||||
<property name="toolTip">
|
||||
|
@ -283,7 +303,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="animSourcesCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html></string>
|
||||
|
|
Loading…
Reference in a new issue