1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-10-17 00:46:34 +00:00

Merge branch openmw:master into master

This commit is contained in:
Igilq 2025-07-28 16:34:40 +00:00
commit 3275b357ac
40 changed files with 849 additions and 592 deletions

View file

@ -1,6 +1,7 @@
Checks: >
-*,
portability-*,
-portability-template-virtual-member-function,
clang-analyzer-*,
-clang-analyzer-optin.*,
-clang-analyzer-cplusplus.NewDeleteLeaks,
@ -16,4 +17,4 @@ CheckOptions:
- key: readability-identifier-naming.NamespaceCase
value: CamelCase
- key: readability-identifier-naming.NamespaceIgnoredRegexp
value: 'osg(DB|FX|Particle|Shadow|Viewer|Util)?'
value: 'bpo|osg(DB|FX|Particle|Shadow|Viewer|Util)?'

View file

@ -139,7 +139,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty)
{
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::NavMeshNotFound);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>());
}
@ -147,7 +147,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception)
{
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::StartPolygonNotFound);
}
@ -156,7 +156,7 @@ namespace
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->removeAgent(mAgentBounds);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::StartPolygonNotFound);
}
@ -172,7 +172,7 @@ namespace
updateGuard.reset();
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -194,7 +194,7 @@ namespace
updateGuard.reset();
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath;
@ -218,7 +218,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -237,7 +237,7 @@ namespace
mPath.clear();
mOut = std::back_inserter(mPath);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -265,7 +265,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -285,7 +285,7 @@ namespace
mPath.clear();
mOut = std::back_inserter(mPath);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -318,7 +318,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -386,7 +386,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -421,7 +421,7 @@ namespace
mEnd.x() = 256;
mEnd.z() = 300;
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -453,8 +453,8 @@ namespace
mStart.x() = 256;
mEnd.x() = 256;
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
{}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -487,8 +487,8 @@ namespace
mStart.x() = 256;
mEnd.x() = 256;
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
{}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -520,7 +520,7 @@ namespace
mStart.x() = 256;
mEnd.x() = 256;
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -549,7 +549,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -577,7 +577,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -658,7 +658,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -781,7 +781,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -806,7 +806,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::PartialPath);
EXPECT_THAT(mPath,
@ -834,7 +834,7 @@ namespace
const float endTolerance = 1000.0f;
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -979,6 +979,146 @@ namespace
EXPECT_EQ(usedNavMeshTiles, 854);
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125),
Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125),
Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125),
Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125)))
<< mPath;
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_cliffs)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, -1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 8.66659259796142578125),
Vec3fEq(385.33331298828125, 79.33331298828125, 8.66659259796142578125),
Vec3fEq(430.666656494140625, 124.66664886474609375, 8.66659259796142578125),
Vec3fEq(463.999969482421875, 463.999969482421875, 8.66659259796142578125)))
<< mPath;
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_with_checkpoints)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const std::vector<osg::Vec3f> checkpoints = {
osg::Vec3f(400, 70, 12),
};
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125),
Vec3fEq(400, 70, 11.33333301544189453125),
Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125),
Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125)))
<< mPath;
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_skip_unreachable_checkpoints)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const std::vector<osg::Vec3f> checkpoints = {
osg::Vec3f(400, 70, 10000),
osg::Vec3f(256, 256, 1000),
osg::Vec3f(-1000, -1000, 0),
};
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125),
Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125),
Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125),
Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125)))
<< mPath;
}
struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam<AgentBounds>
{
};

View file

@ -9,8 +9,6 @@
#include <fstream>
#include <iostream>
namespace sfs = std::filesystem;
namespace
{
// from configfileparser.cpp

View file

@ -10,7 +10,6 @@
#include <components/files/conversion.hpp>
namespace bpo = boost::program_options;
namespace sfs = std::filesystem;
#ifndef _WIN32
int main(int argc, char* argv[])

View file

@ -589,8 +589,7 @@ namespace MWBase
virtual bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0;
virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors = nullptr) const = 0;
virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0;
virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0;

View file

@ -233,6 +233,7 @@ namespace MWLua
DetourNavigator::Flags includeFlags = defaultIncludeFlags;
DetourNavigator::AreaCosts areaCosts{};
float destinationTolerance = 1;
std::vector<osg::Vec3f> checkpoints;
if (options.has_value())
{
@ -258,13 +259,24 @@ namespace MWLua
}
if (const auto& v = options->get<sol::optional<float>>("destinationTolerance"))
destinationTolerance = *v;
if (const auto& t = options->get<sol::optional<sol::table>>("checkpoints"))
{
for (const auto& [k, v] : *t)
{
const int index = k.as<int>();
const osg::Vec3f position = v.as<osg::Vec3f>();
if (index != static_cast<int>(checkpoints.size() + 1))
throw std::runtime_error("checkpoints is not an array");
checkpoints.push_back(position);
}
}
}
std::vector<osg::Vec3f> path;
const DetourNavigator::Status status
= DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds,
source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path));
const DetourNavigator::Status status = DetourNavigator::findPath(
*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination,
includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path));
sol::table result(lua, sol::create);
LuaUtil::copyVectorToTable(path, result);

View file

@ -73,6 +73,21 @@ namespace
namespace MWMechanics
{
struct ActiveSpells::UpdateContext
{
bool mUpdatedEnemy = false;
bool mUpdatedHitOverlay = false;
bool mUpdateSpellWindow = false;
bool mPlayNonLooping = false;
bool mEraseRemoved = false;
bool mUpdate;
UpdateContext(bool update)
: mUpdate(update)
{
}
};
ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells)
: mActiveSpells(spells)
{
@ -256,8 +271,9 @@ namespace MWMechanics
++spellIt;
}
UpdateContext context(duration > 0.f);
for (const auto& spell : mQueue)
addToSpells(ptr, spell);
addToSpells(ptr, spell, context);
mQueue.clear();
// Vanilla only does this on cell change I think
@ -267,20 +283,17 @@ namespace MWMechanics
if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power
&& !isSpellActive(spell->mId))
{
mSpells.emplace_back(ActiveSpellParams{ spell, ptr, true });
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
initParams(ptr, ActiveSpellParams{ spell, ptr, true }, context);
}
}
bool updateSpellWindow = false;
bool playNonLooping = false;
if (ptr.getClass().hasInventoryStore(ptr)
&& !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()))
{
auto& store = ptr.getClass().getInventoryStore(ptr);
if (store.getInvListener() != nullptr)
{
playNonLooping = !store.isFirstEquip();
context.mPlayNonLooping = !store.isFirstEquip();
const auto world = MWBase::Environment::get().getWorld();
for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++)
{
@ -306,82 +319,108 @@ namespace MWMechanics
// invisibility manually
purgeEffect(ptr, ESM::MagicEffect::Invisibility);
applyPurges(ptr);
ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr });
params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
updateSpellWindow = true;
ActiveSpellParams* params = initParams(ptr, ActiveSpellParams{ *slot, enchantment, ptr }, context);
if (params)
context.mUpdateSpellWindow = true;
}
}
}
const MWWorld::Ptr player = MWMechanics::getPlayer();
bool updatedHitOverlay = false;
bool updatedEnemy = false;
// Update effects
context.mEraseRemoved = true;
for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
{
const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(
spellIt->mCasterActorId); // Maybe make this search outside active grid?
bool removedSpell = false;
std::optional<ActiveSpellParams> reflected;
for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
{
auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, playNonLooping);
if (result.mType == MagicApplicationResult::Type::REFLECTED)
{
if (!reflected)
{
if (Settings::game().mClassicReflectedAbsorbSpellsBehavior)
reflected = { *spellIt, caster };
else
reflected = { *spellIt, ptr };
}
auto& reflectedEffect = reflected->mEffects.emplace_back(*it);
reflectedEffect.mFlags
= ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
it = spellIt->mEffects.erase(it);
}
else if (result.mType == MagicApplicationResult::Type::REMOVED)
it = spellIt->mEffects.erase(it);
else
{
++it;
if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player)
{
MWBase::Environment::get().getWindowManager()->setEnemy(ptr);
updatedEnemy = true;
}
if (!updatedHitOverlay && result.mShowHit && ptr == player)
{
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
updatedHitOverlay = true;
}
}
removedSpell = applyPurges(ptr, &spellIt, &it);
if (removedSpell)
break;
}
if (reflected)
{
const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get<ESM::Static>().find(
ESM::RefId::stringRefId("VFX_Reflect"));
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr);
if (animation && !reflectStatic->mModel.empty())
{
const VFS::Path::Normalized reflectStaticModel
= Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel));
animation->addEffect(
reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false);
}
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected);
}
if (removedSpell)
continue;
updateActiveSpell(ptr, duration, spellIt, context);
}
if (Settings::game().mClassicCalmSpellsBehavior)
{
ESM::MagicEffect::Effects effect
= ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature;
if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f)
creatureStats.getAiSequence().stopCombat();
}
if (ptr == player && context.mUpdateSpellWindow)
{
// Something happened with the spell list -- possibly while the game is paused,
// so we want to make the spell window get the memo.
// We don't normally want to do this, so this targets constant enchantments.
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
}
}
bool ActiveSpells::updateActiveSpell(
const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context)
{
const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(
spellIt->mCasterActorId); // Maybe make this search outside active grid?
bool removedSpell = false;
std::optional<ActiveSpellParams> reflected;
for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
{
auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, context.mPlayNonLooping);
if (result.mType == MagicApplicationResult::Type::REFLECTED)
{
if (!reflected)
{
if (Settings::game().mClassicReflectedAbsorbSpellsBehavior)
reflected = { *spellIt, caster };
else
reflected = { *spellIt, ptr };
}
auto& reflectedEffect = reflected->mEffects.emplace_back(*it);
reflectedEffect.mFlags
= ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
it = spellIt->mEffects.erase(it);
}
else if (result.mType == MagicApplicationResult::Type::REMOVED)
it = spellIt->mEffects.erase(it);
else
{
const MWWorld::Ptr player = MWMechanics::getPlayer();
++it;
if (!context.mUpdatedEnemy && result.mShowHealth && caster == player && ptr != player)
{
MWBase::Environment::get().getWindowManager()->setEnemy(ptr);
context.mUpdatedEnemy = true;
}
if (!context.mUpdatedHitOverlay && result.mShowHit && ptr == player)
{
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
context.mUpdatedHitOverlay = true;
}
}
removedSpell = applyPurges(ptr, &spellIt, &it);
if (removedSpell)
break;
}
if (reflected)
{
const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get<ESM::Static>().find(
ESM::RefId::stringRefId("VFX_Reflect"));
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr);
if (animation && !reflectStatic->mModel.empty())
{
const VFS::Path::Normalized reflectStaticModel
= Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel));
animation->addEffect(
reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false);
}
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected);
}
if (removedSpell)
return true;
if (context.mEraseRemoved)
{
bool remove = false;
if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore))
{
try
{
auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells();
remove = !spells.hasSpell(spellIt->mSourceSpellId);
}
catch (const std::runtime_error& e)
@ -413,30 +452,27 @@ namespace MWMechanics
for (const auto& effect : params.mEffects)
onMagicEffectRemoved(ptr, params, effect);
applyPurges(ptr, &spellIt);
updateSpellWindow = true;
continue;
context.mUpdateSpellWindow = true;
return true;
}
++spellIt;
}
if (Settings::game().mClassicCalmSpellsBehavior)
{
ESM::MagicEffect::Effects effect
= ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature;
if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f)
creatureStats.getAiSequence().stopCombat();
}
if (ptr == player && updateSpellWindow)
{
// Something happened with the spell list -- possibly while the game is paused,
// so we want to make the spell window get the memo.
// We don't normally want to do this, so this targets constant enchantments.
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
}
++spellIt;
return false;
}
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell)
ActiveSpells::ActiveSpellParams* ActiveSpells::initParams(
const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context)
{
mSpells.emplace_back(params).setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
auto it = mSpells.end();
--it;
// We instantly apply the effect with a duration of 0 so continuous effects can be purged before truly applying
if (context.mUpdate && updateActiveSpell(ptr, 0.f, it, context))
return nullptr;
return &*it;
}
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context)
{
if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable))
{
@ -454,8 +490,7 @@ namespace MWMechanics
onMagicEffectRemoved(ptr, params, effect);
}
}
mSpells.emplace_back(spell);
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
initParams(ptr, spell, context);
}
ActiveSpells::ActiveSpells()
@ -608,6 +643,8 @@ namespace MWMechanics
{
purge(
[=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) {
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied))
return false;
if (effectArg.empty())
return effect.mEffectId == effectId;
return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg;

View file

@ -116,17 +116,23 @@ namespace MWMechanics
IterationGuard(ActiveSpells& spells);
~IterationGuard();
};
struct UpdateContext;
std::list<ActiveSpellParams> mSpells;
std::vector<ActiveSpellParams> mQueue;
std::queue<Predicate> mPurges;
bool mIterating;
void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell);
void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context);
bool applyPurges(const MWWorld::Ptr& ptr, std::list<ActiveSpellParams>::iterator* currentSpell = nullptr,
std::vector<ActiveEffect>::iterator* currentEffect = nullptr);
bool updateActiveSpell(
const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context);
ActiveSpellParams* initParams(const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context);
public:
ActiveSpells();

View file

@ -1326,19 +1326,37 @@ namespace MWMechanics
const MWWorld::Ptr player = getPlayer();
const MWBase::World* const world = MWBase::Environment::get().getWorld();
struct CacheEntry
{
MWWorld::Ptr mPtr;
float mMaxSpeed;
osg::Vec3f mHalfExtents;
Movement& mMovement;
};
std::vector<CacheEntry> cache;
cache.reserve(mActors.size());
for (const Actor& actor : mActors)
{
if (actor.isInvalid())
continue;
const MWWorld::Ptr& ptr = actor.getPtr();
const MWWorld::Class& cls = ptr.getClass();
cache.push_back({ ptr, cls.getMaxSpeed(ptr), world->getHalfExtents(ptr), cls.getMovementSettings(ptr) });
}
for (const CacheEntry& cached : cache)
{
const MWWorld::Ptr& ptr = cached.mPtr;
if (ptr == player)
continue; // Don't interfere with player controls.
const float maxSpeed = ptr.getClass().getMaxSpeed(ptr);
const float maxSpeed = cached.mMaxSpeed;
if (maxSpeed == 0.0)
continue; // Can't move, so there is no sense to predict collisions.
Movement& movement = ptr.getClass().getMovementSettings(ptr);
Movement& movement = cached.mMovement;
const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]);
const bool isMoving = origMovement.length2() > 0.01;
if (movement.mPosition[1] < 0)
@ -1379,7 +1397,7 @@ namespace MWMechanics
const osg::Vec2f baseSpeed = origMovement * maxSpeed;
const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3();
const float baseRotZ = ptr.getRefData().getPosition().rot[2];
const osg::Vec3f halfExtents = world->getHalfExtents(ptr);
const osg::Vec3f& halfExtents = cached.mHalfExtents;
const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding;
float timeToCheck = maxTimeToCheck;
@ -1392,15 +1410,13 @@ namespace MWMechanics
float angleToApproachingActor = 0;
// Iterate through all other actors and predict collisions.
for (const Actor& otherActor : mActors)
for (const CacheEntry& otherCached : cache)
{
if (otherActor.isInvalid())
continue;
const MWWorld::Ptr& otherPtr = otherActor.getPtr();
const MWWorld::Ptr& otherPtr = otherCached.mPtr;
if (otherPtr == ptr || otherPtr == currentTarget)
continue;
const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr);
const osg::Vec3f& otherHalfExtents = otherCached.mHalfExtents;
const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos;
const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ);
const float dist = deltaPos.length();
@ -1413,8 +1429,7 @@ namespace MWMechanics
if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2)
continue;
const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3()
* otherPtr.getClass().getMaxSpeed(otherPtr);
const osg::Vec3f speed = otherCached.mMovement.asVec3() * otherCached.mMaxSpeed;
const float rotZ = otherPtr.getRefData().getPosition().rot[2];
const osg::Vec2f relSpeed
= Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed;

View file

@ -1,13 +1,11 @@
#include "aicombat.hpp"
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/rng.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/mathutil.hpp>
#include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/mathutil.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include "../mwphysics/raycasting.hpp"
@ -178,11 +176,16 @@ namespace MWMechanics
currentCell = actor.getCell();
}
const MWWorld::Class& actorClass = actor.getClass();
MWMechanics::CreatureStats& stats = actorClass.getCreatureStats(actor);
if (stats.isParalyzed() || stats.getKnockedDown())
return false;
bool forceFlee = false;
if (!canFight(actor, target))
{
storage.stopAttack();
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false);
stats.setAttackingOrSpell(false);
storage.mActionCooldown = 0.f;
// Continue combat if target is player or player follower/escorter and an attack has been attempted
const auto& playerFollowersAndEscorters
@ -191,18 +194,14 @@ namespace MWMechanics
= (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target)
!= playerFollowersAndEscorters.end());
if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer)
&& ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId()
== target.getClass().getCreatureStats(target).getActorId())
|| (target.getClass().getCreatureStats(target).getHitAttemptActorId()
== actor.getClass().getCreatureStats(actor).getActorId())))
&& ((stats.getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId())
|| (target.getClass().getCreatureStats(target).getHitAttemptActorId() == stats.getActorId())))
forceFlee = true;
else // Otherwise end combat
return true;
}
const MWWorld::Class& actorClass = actor.getClass();
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
stats.setMovementFlag(CreatureStats::Flag_Run, true);
float& actionCooldown = storage.mActionCooldown;
std::unique_ptr<Action>& currentAction = storage.mCurrentAction;
@ -302,8 +301,8 @@ namespace MWMechanics
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const auto& pathGridGraph = getPathGridGraph(pathgrid);
mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full);
mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
storage.mAttackRange, PathType::Full);
if (!mPathFinder.isPathConstructed())
{
@ -316,8 +315,8 @@ namespace MWMechanics
if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack)
{
// If the point is close enough, try to find a path to that point.
mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full);
mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
storage.mAttackRange, PathType::Full);
if (mPathFinder.isPathConstructed())
{
// If path to that point is found use it as custom destination.
@ -330,7 +329,7 @@ namespace MWMechanics
{
storage.mUseCustomDestination = false;
storage.stopAttack();
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false);
stats.setAttackingOrSpell(false);
currentAction = std::make_unique<ActionFlee>();
actionCooldown = currentAction->getActionCooldown();
storage.startFleeing();
@ -393,8 +392,8 @@ namespace MWMechanics
osg::Vec3f localPos = actor.getRefData().getPosition().asVec3();
coords.toLocal(localPos);
size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos);
for (size_t i = 0; i < pathgrid->mPoints.size(); i++)
const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos);
for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++)
{
if (i != closestPointIndex
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i))
@ -455,7 +454,8 @@ namespace MWMechanics
float dist
= (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
if ((dist > fFleeDistance && !storage.mLOS)
|| pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections))
|| pathTo(
actor, Misc::Convert::makeOsgVec3f(storage.mFleeDest), duration, supportedMovementDirections))
{
state = AiCombatStorage::FleeState_Idle;
}

View file

@ -2,12 +2,13 @@
#define GAME_MWMECHANICS_AICOMBAT_H
#include "aitemporarybase.hpp"
#include "aitimer.hpp"
#include "movement.hpp"
#include "typedaipackage.hpp"
#include "../mwworld/cellstore.hpp" // for Doors
#include "aitimer.hpp"
#include "movement.hpp"
#include <components/esm3/loadpgrd.hpp>
namespace ESM
{

View file

@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
= world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid),
agentBounds, navigatorFlags, areaCosts, endTolerance, pathType);
mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds,
navigatorFlags, areaCosts, endTolerance, pathType);
mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity
@ -501,7 +501,11 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::
result |= DetourNavigator::Flag_swim;
if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0)
result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid;
{
result |= DetourNavigator::Flag_walk;
if (getTypeId() != AiPackageTypeId::Wander)
result |= DetourNavigator::Flag_usePathgrid;
}
if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander)
result |= DetourNavigator::Flag_openDoor;

View file

@ -16,6 +16,8 @@
namespace ESM
{
struct Cell;
struct Pathgrid;
namespace AiSequence
{
struct AiSequence;

View file

@ -8,6 +8,7 @@
#include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp"
@ -29,17 +30,6 @@
namespace MWMechanics
{
static const int COUNT_BEFORE_RESET = 10;
static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f;
// to prevent overcrowding
static const int DESTINATION_TOLERANCE = 64;
// distance must be long enough that NPC will need to move to get there.
static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2;
static const std::size_t MAX_IDLE_SIZE = 8;
const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = {
"idle2",
"idle3",
@ -53,11 +43,22 @@ namespace MWMechanics
namespace
{
constexpr int countBeforeReset = 10;
constexpr float idlePositionCheckInterval = 1.5f;
// to prevent overcrowding
constexpr unsigned destinationTolerance = 64;
// distance must be long enough that NPC will need to move to get there.
constexpr unsigned minimumWanderDistance = destinationTolerance * 2;
constexpr std::size_t maxIdleSize = 8;
inline int getCountBeforeReset(const MWWorld::ConstPtr& actor)
{
if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor))
return 1;
return COUNT_BEFORE_RESET;
return countBeforeReset;
}
osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance)
@ -99,16 +100,42 @@ namespace MWMechanics
std::vector<unsigned char> getInitialIdle(const std::vector<unsigned char>& idle)
{
std::vector<unsigned char> result(MAX_IDLE_SIZE, 0);
std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin());
std::vector<unsigned char> result(maxIdleSize, 0);
std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin());
return result;
}
std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE])
std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[maxIdleSize])
{
return std::vector<unsigned char>(std::begin(idle), std::end(idle));
}
void trimAllowedPositions(const std::deque<osg::Vec3f>& path, std::vector<osg::Vec3f>& allowedPositions)
{
// TODO: how to add these back in once the door opens?
// Idea: keep a list of detected closed doors (see aicombat.cpp)
// Every now and then check whether one of the doors is opened. (maybe
// at the end of playing idle?) If the door is opened then re-calculate
// allowed positions starting from the spawn point.
std::vector<osg::Vec3f> points(path.begin(), path.end());
while (points.size() >= 2)
{
const osg::Vec3f point = points.back();
for (std::size_t j = 0; j < allowedPositions.size(); j++)
{
// FIXME: doesn't handle a door with the same X/Y
// coordinates but with a different Z
if (std::abs(allowedPositions[j].x() - point.x()) <= 0.5
&& std::abs(allowedPositions[j].y() - point.y()) <= 0.5)
{
allowedPositions.erase(allowedPositions.begin() + j);
break;
}
}
points.pop_back();
}
}
}
AiWanderStorage::AiWanderStorage()
@ -118,9 +145,9 @@ namespace MWMechanics
, mCanWanderAlongPathGrid(true)
, mIdleAnimation(0)
, mBadIdles()
, mPopulateAvailableNodes(true)
, mAllowedNodes()
, mTrimCurrentNode(false)
, mPopulateAvailablePositions(true)
, mAllowedPositions()
, mTrimCurrentPosition(false)
, mCheckIdlePositionTimer(0)
, mStuckCount(0)
{
@ -128,8 +155,8 @@ namespace MWMechanics
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat)
: TypedAiPackage<AiWander>(repeat)
, mDistance(std::max(0, distance))
, mDuration(std::max(0, duration))
, mDistance(static_cast<unsigned>(std::max(0, distance)))
, mDuration(static_cast<unsigned>(std::max(0, duration)))
, mRemainingDuration(duration)
, mTimeOfDay(timeOfDay)
, mIdle(getInitialIdle(idle))
@ -215,20 +242,12 @@ namespace MWMechanics
{
const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
if (mUsePathgrid)
{
mPathFinder.buildPathByPathgrid(
pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid));
}
else
{
const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor);
constexpr float endTolerance = 0;
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid),
agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full);
}
const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor);
constexpr float endTolerance = 0;
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds,
navigatorFlags, areaCosts, endTolerance, PathType::Full);
if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid);
@ -259,9 +278,6 @@ namespace MWMechanics
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
{
if (mDistance <= 0)
storage.mCanWanderAlongPathGrid = false;
if (isPackageCompleted())
{
stopWalking(actor);
@ -276,13 +292,15 @@ namespace MWMechanics
mStoredInitialActorPosition = true;
}
// Initialization to discover & store allowed node points for this actor.
if (storage.mPopulateAvailableNodes)
// Initialization to discover & store allowed positions points for this actor.
if (storage.mPopulateAvailablePositions)
{
getAllowedNodes(actor, storage);
fillAllowedPositions(actor, storage);
}
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
MWBase::World& world = *MWBase::Environment::get().getWorld();
auto& prng = world.getPrng();
if (canActorMoveByZAxis(actor) && mDistance > 0)
{
// Typically want to idle for a short time before the next wander
@ -295,7 +313,7 @@ namespace MWMechanics
}
// If the package has a wander distance but no pathgrid is available,
// randomly idle or wander near spawn point
else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually)
else if (storage.mAllowedPositions.empty() && mDistance > 0 && !storage.mIsWanderingManually)
{
// Typically want to idle for a short time before the next wander
if (Misc::Rng::rollDice(100, prng) >= 96)
@ -307,7 +325,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow);
}
}
else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually)
else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually)
{
storage.mCanWanderAlongPathGrid = false;
}
@ -323,9 +341,9 @@ namespace MWMechanics
// Construct a new path if there isn't one
if (!mPathFinder.isPathConstructed())
{
if (!storage.mAllowedNodes.empty())
if (!storage.mAllowedPositions.empty())
{
setPathToAnAllowedNode(actor, storage, pos);
setPathToAnAllowedPosition(actor, storage, pos);
}
}
}
@ -336,7 +354,7 @@ namespace MWMechanics
if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking
&& (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back())
|| isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
|| world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
completeManualWalking(actor, storage);
return false; // AiWander package not yet completed
@ -366,12 +384,12 @@ namespace MWMechanics
std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
const auto world = MWBase::Environment::get().getWorld();
const auto agentBounds = world->getPathfindingAgentBounds(actor);
const auto navigator = world->getNavigator();
MWBase::World& world = *MWBase::Environment::get().getWorld();
const auto agentBounds = world.getPathfindingAgentBounds(actor);
const auto navigator = world.getNavigator();
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
Misc::Rng::Generator& prng = world.getPrng();
do
{
@ -411,7 +429,7 @@ namespace MWMechanics
if (isDestinationHidden(actor, mDestination))
continue;
if (isAreaOccupiedByOtherActor(actor, mDestination))
if (world.isAreaOccupiedByOtherActor(actor, mDestination))
continue;
constexpr float endTolerance = 0;
@ -491,17 +509,17 @@ namespace MWMechanics
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{
// Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking.
// Check if an idle actor is too far from all allowed positions or too close to a door - if so start walking.
storage.mCheckIdlePositionTimer += duration;
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary())
if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary())
{
storage.mCheckIdlePositionTimer = 0; // restart timer
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f;
if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance))
if (proximityToDoor(actor, distance) || !isNearAllowedPosition(actor, storage, distance))
{
storage.setState(AiWanderStorage::Wander_MoveNow);
storage.mTrimCurrentNode = false; // just in case
storage.mTrimCurrentPosition = false; // just in case
return;
}
}
@ -517,16 +535,14 @@ namespace MWMechanics
}
}
bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const
bool AiWander::isNearAllowedPosition(
const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const
{
const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes)
{
osg::Vec3f point(node.mX, node.mY, node.mZ);
if ((actorPos - point).length2() < distance * distance)
return true;
}
return false;
const float squaredDistance = distance * distance;
return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) {
return (actorPos - v).length2() < squaredDistance;
}) != storage.mAllowedPositions.end();
}
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration,
@ -535,7 +551,7 @@ namespace MWMechanics
// Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed())
|| pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections,
DESTINATION_TOLERANCE))
destinationTolerance))
{
stopWalking(actor);
storage.setState(AiWanderStorage::Wander_ChooseAction);
@ -586,8 +602,8 @@ namespace MWMechanics
if (proximityToDoor(actor, distance))
{
// remove allowed points then select another random destination
storage.mTrimCurrentNode = true;
trimAllowedNodes(storage.mAllowedNodes, mPathFinder);
storage.mTrimCurrentPosition = true;
trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions);
mObstacleCheck.clear();
stopWalking(actor);
storage.setState(AiWanderStorage::Wander_MoveNow);
@ -606,67 +622,67 @@ namespace MWMechanics
}
}
void AiWander::setPathToAnAllowedNode(
void AiWander::setPathToAnAllowedPosition(
const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos)
{
auto world = MWBase::Environment::get().getWorld();
auto& prng = world->getPrng();
unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng);
const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode];
MWBase::World& world = *MWBase::Environment::get().getWorld();
Misc::Rng::Generator& prng = world.getPrng();
const std::size_t randomAllowedPositionIndex
= static_cast<std::size_t>(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng));
const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex];
const osg::Vec3f start = actorPos.asVec3();
// don't take shortcuts for wandering
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest);
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid));
const MWWorld::Cell& cell = *actor.getCell()->getCell();
const ESM::Pathgrid* pathgrid = world.getStore().get<ESM::Pathgrid>().search(cell);
const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid);
if (mPathFinder.isPathConstructed())
const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell);
std::deque<ESM::Pathgrid::Point> path
= pathgridGraph.aStarSearch(Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(start)),
Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(randomAllowedPosition)));
// Choose a different position and delete this one from possible positions because it is uncreachable:
if (path.empty())
{
mDestination = destVec3f;
mHasDestination = true;
mUsePathgrid = true;
// Remove this node as an option and add back the previously used node (stops NPC from picking the same
// node):
ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
// check if mCurrentNode was taken out of mAllowedNodes
if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1)
storage.mTrimCurrentNode = false;
else
storage.mAllowedNodes.push_back(storage.mCurrentNode);
storage.mCurrentNode = temp;
storage.setState(AiWanderStorage::Wander_Walking);
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
return;
}
// Choose a different node and delete this one from possible nodes because it is uncreachable:
// Drop nearest pathgrid point.
path.pop_front();
std::vector<osg::Vec3f> checkpoints(path.size());
for (std::size_t i = 0; i < path.size(); ++i)
checkpoints[i] = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(path[i]));
const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(actor);
const DetourNavigator::Flags flags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, flags);
constexpr float endTolerance = 0;
mPathFinder.buildPath(actor, start, randomAllowedPosition, pathgridGraph, agentBounds, flags, areaCosts,
endTolerance, PathType::Full, checkpoints);
if (!mPathFinder.isPathConstructed())
{
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
return;
}
mDestination = randomAllowedPosition;
mHasDestination = true;
mUsePathgrid = true;
// Remove this position as an option and add back the previously used position (stops NPC from picking the
// same position):
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
// check if mCurrentPosition was taken out of mAllowedPositions
if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1)
storage.mTrimCurrentPosition = false;
else
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
}
storage.mAllowedPositions.push_back(storage.mCurrentPosition);
storage.mCurrentPosition = randomAllowedPosition;
void AiWander::trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder)
{
// TODO: how to add these back in once the door opens?
// Idea: keep a list of detected closed doors (see aicombat.cpp)
// Every now and then check whether one of the doors is opened. (maybe
// at the end of playing idle?) If the door is opened then re-calculate
// allowed nodes starting from the spawn point.
auto paths = pathfinder.getPath();
while (paths.size() >= 2)
{
const auto pt = paths.back();
for (unsigned int j = 0; j < nodes.size(); j++)
{
// FIXME: doesn't handle a door with the same X/Y
// coordinates but with a different Z
if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5)
{
nodes.erase(nodes.begin() + j);
break;
}
}
paths.pop_back();
}
storage.setState(AiWanderStorage::Wander_Walking);
}
void AiWander::stopWalking(const MWWorld::Ptr& actor)
@ -742,20 +758,20 @@ namespace MWMechanics
return;
AiWanderStorage& storage = state.get<AiWanderStorage>();
if (storage.mPopulateAvailableNodes)
getAllowedNodes(actor, storage);
if (storage.mPopulateAvailablePositions)
fillAllowedPositions(actor, storage);
if (storage.mAllowedNodes.empty())
if (storage.mAllowedPositions.empty())
return;
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng);
ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index];
int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng);
const osg::Vec3f worldDest = storage.mAllowedPositions[index];
const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell());
ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest);
osg::Vec3f dest = converter.toLocalVec3(worldDest);
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(
PathFinder::makeOsgVec3(worldDest), 60);
const bool isPathGridOccupied
= MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60);
// add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied)
@ -775,19 +791,17 @@ namespace MWMechanics
const ESM::Pathgrid::Point& connDest = points[randomIndex];
// add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest);
float length = dir.length();
osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest;
const float length = dir.length();
dir.normalize();
for (int j = 1; j <= 3; j++)
{
// move for 5-15% towards random neighboring node
dest
= PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = converter.toWorldPoint(dest);
dest = dest + dir * (j * 5 * length / 100.f);
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(
PathFinder::makeOsgVec3(worldDest), 60);
converter.toWorldVec3(dest), 60);
if (!isOccupied)
break;
@ -807,19 +821,18 @@ namespace MWMechanics
// place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be
// underground. Adding 20 in adjustPosition() is not enough.
dest.mZ += 60;
dest.z() += 60;
converter.toWorld(dest);
state.reset();
osg::Vec3f pos(static_cast<float>(dest.mX), static_cast<float>(dest.mY), static_cast<float>(dest.mZ));
MWBase::Environment::get().getWorld()->moveObject(actor, pos);
MWBase::Environment::get().getWorld()->moveObject(actor, dest);
actor.getClass().adjustPosition(actor, false);
}
void AiWander::getNeighbouringNodes(
ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points)
const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points)
{
const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*currentCell->getCell());
@ -827,19 +840,19 @@ namespace MWMechanics
if (pathgrid == nullptr || pathgrid->mPoints.empty())
return;
size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest));
const size_t index = Misc::getClosestPoint(*pathgrid, dest);
getPathGridGraph(pathgrid).getNeighbouringPoints(index, points);
}
void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage)
void AiWander::fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{
// infrequently used, therefore no benefit in caching it as a member
const MWWorld::CellStore* cellStore = actor.getCell();
const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*cellStore->getCell());
storage.mAllowedNodes.clear();
storage.mAllowedPositions.clear();
// If there is no path this actor doesn't go anywhere. See:
// https://forum.openmw.org/viewtopic.php?t=1556
@ -860,34 +873,35 @@ namespace MWMechanics
const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition);
// Find closest pathgrid point
size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos);
const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, npcPos);
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
// mAllowedPositions for this actor with pathgrid point indexes based on mDistance
// and if the point is connected to the closest current point
// NOTE: mPoints is in local coordinates
size_t pointIndex = 0;
for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++)
{
osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter]));
const osg::Vec3f nodePos = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[counter]);
if ((npcPos - nodePos).length2() <= mDistance * mDistance
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter))
{
storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter]));
storage.mAllowedPositions.push_back(
Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid->mPoints[counter])));
pointIndex = counter;
}
}
if (storage.mAllowedNodes.size() == 1)
if (storage.mAllowedPositions.size() == 1)
{
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition));
storage.mAllowedPositions.push_back(mInitialActorPosition);
addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter);
}
if (!storage.mAllowedNodes.empty())
if (!storage.mAllowedPositions.empty())
{
setCurrentNodeToClosestAllowedNode(storage);
setCurrentPositionToClosestAllowedPosition(storage);
}
}
storage.mPopulateAvailableNodes = false;
storage.mPopulateAvailablePositions = false;
}
// When only one path grid point in wander distance,
@ -901,44 +915,44 @@ namespace MWMechanics
{
if (edge.mV0 == pointIndex)
{
AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]),
addPositionBetweenPathgridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]),
converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage);
}
}
}
void AiWander::AddPointBetweenPathGridPoints(
void AiWander::addPositionBetweenPathgridPoints(
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage)
{
osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start);
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart;
osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start);
osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart;
float length = delta.length();
delta.normalize();
int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE);
unsigned distance = std::max(mDistance / 2, minimumWanderDistance);
// must not travel longer than distance between waypoints or NPC goes past waypoint
distance = std::min(distance, static_cast<int>(length));
distance = std::min(distance, static_cast<unsigned>(length));
delta *= distance;
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta));
storage.mAllowedPositions.push_back(vectorStart + delta);
}
void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage)
void AiWander::setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage)
{
float distanceToClosestNode = std::numeric_limits<float>::max();
float distanceToClosestPosition = std::numeric_limits<float>::max();
size_t index = 0;
for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i)
for (size_t i = 0; i < storage.mAllowedPositions.size(); ++i)
{
osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i]));
float tempDist = (mInitialActorPosition - nodePos).length2();
if (tempDist < distanceToClosestNode)
const osg::Vec3f position = storage.mAllowedPositions[i];
const float tempDist = (mInitialActorPosition - position).length2();
if (tempDist < distanceToClosestPosition)
{
index = i;
distanceToClosestNode = tempDist;
distanceToClosestPosition = tempDist;
}
}
storage.mCurrentNode = storage.mAllowedNodes[index];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index);
storage.mCurrentPosition = storage.mAllowedPositions[index];
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index);
}
void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const
@ -970,8 +984,8 @@ namespace MWMechanics
AiWander::AiWander(const ESM::AiSequence::AiWander* wander)
: TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0))
, mDistance(std::max(static_cast<short>(0), wander->mData.mDistance))
, mDuration(std::max(static_cast<short>(0), wander->mData.mDuration))
, mDistance(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDistance)))
, mDuration(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDuration)))
, mRemainingDuration(wander->mDurationData.mRemainingDuration)
, mTimeOfDay(wander->mData.mTimeOfDay)
, mIdle(getInitialIdle(wander->mData.mIdle))

View file

@ -1,14 +1,15 @@
#ifndef GAME_MWMECHANICS_AIWANDER_H
#define GAME_MWMECHANICS_AIWANDER_H
#include "typedaipackage.hpp"
#include <string_view>
#include <vector>
#include "aitemporarybase.hpp"
#include "aitimer.hpp"
#include "pathfinding.hpp"
#include "typedaipackage.hpp"
#include <components/esm3/loadpgrd.hpp>
#include <string_view>
#include <vector>
namespace ESM
{
@ -51,14 +52,13 @@ namespace MWMechanics
unsigned short mIdleAnimation;
std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors
// do we need to calculate allowed nodes based on mDistance
bool mPopulateAvailableNodes;
bool mPopulateAvailablePositions;
// allowed pathgrid nodes based on mDistance from the spawn point
std::vector<ESM::Pathgrid::Point> mAllowedNodes;
// allowed destination positions based on mDistance from the spawn point
std::vector<osg::Vec3f> mAllowedPositions;
ESM::Pathgrid::Point mCurrentNode;
bool mTrimCurrentNode;
osg::Vec3f mCurrentPosition;
bool mTrimCurrentPosition;
float mCheckIdlePositionTimer;
int mStuckCount;
@ -132,7 +132,8 @@ namespace MWMechanics
bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
int getRandomIdle() const;
void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos);
void setPathToAnAllowedPosition(
const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos);
void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration,
MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage);
@ -145,28 +146,27 @@ 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;
bool isNearAllowedPosition(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;
// how far the actor can wander from the spawn point
const unsigned mDistance;
const unsigned mDuration;
float mRemainingDuration;
const int mTimeOfDay;
const std::vector<unsigned char> mIdle;
bool mStoredInitialActorPosition;
osg::Vec3f
mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell
// Note: an original engine does not reset coordinates even when actor changes a cell
osg::Vec3f mInitialActorPosition;
bool mHasDestination;
osg::Vec3f mDestination;
bool mUsePathgrid;
void getNeighbouringNodes(
ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points);
const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points);
void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder);
void fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage);
// constants for converting idleSelect values into groupNames
enum GroupIndex
@ -175,12 +175,12 @@ namespace MWMechanics
GroupIndex_MaxIdle = 9
};
void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage);
void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage);
void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage,
const Misc::CoordinateConverter& converter);
void AddPointBetweenPathGridPoints(
void addPositionBetweenPathgridPoints(
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage);
/// lookup table for converting idleSelect value to groupName

View file

@ -2310,17 +2310,13 @@ namespace MWMechanics
}
else
{
// Do not play turning animation for player if rotation speed is very slow.
// Actual threshold should take framerate in account.
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.
if (!sneak && !isFirstPersonPlayer && isBiped)
{
if (effectiveRotation > rotationThreshold)
if (effectiveRotation > 0.f)
movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight;
else if (effectiveRotation < -rotationThreshold)
else if (effectiveRotation < 0.f)
movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft;
}
}
@ -2346,34 +2342,19 @@ namespace MWMechanics
vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef);
}
// Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering
if (isPlayer)
if (isBiped)
{
float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f;
float complete;
bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete);
if (movestate == CharState_None && jumpstate == JumpState_None && isTurning())
{
if (animPlaying && complete < threshold)
movestate = mMovementState;
}
}
else
{
if (isBiped)
{
if (mTurnAnimationThreshold > 0)
mTurnAnimationThreshold -= duration;
if (mTurnAnimationThreshold > 0)
mTurnAnimationThreshold -= duration;
if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft
|| movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft)
{
mTurnAnimationThreshold = 0.05f;
}
else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0)
{
movestate = mMovementState;
}
if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft
|| movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft)
{
mTurnAnimationThreshold = 0.05f;
}
else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0)
{
movestate = mMovementState;
}
}
@ -2402,11 +2383,10 @@ namespace MWMechanics
if (isTurning())
{
// Adjust animation speed from 1.0 to 1.5 multiplier
if (duration > 0)
{
float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast<float>(osg::PI));
mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f));
mAnimation->adjustSpeedMult(mCurrentMovement, turnSpeed);
}
}
else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed)

View file

@ -106,21 +106,6 @@ namespace MWMechanics
return visitor.mResult;
}
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer,
std::vector<MWWorld::Ptr>* occupyingActors)
{
const auto world = MWBase::Environment::get().getWorld();
const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents;
const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
if (ignorePlayer)
{
const std::array ignore{ actor, world->getPlayerConstPtr() };
return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors);
}
const std::array ignore{ actor };
return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors);
}
ObstacleCheck::ObstacleCheck()
: mEvadeDirectionIndex(std::size(evadeDirections) - 1)
{

View file

@ -5,8 +5,6 @@
#include <osg/Vec3f>
#include <vector>
namespace MWWorld
{
class Ptr;
@ -24,9 +22,6 @@ namespace MWMechanics
/** \return Pointer to the door, or empty pointer if none exists **/
const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist);
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination,
bool ignorePlayer = false, std::vector<MWWorld::Ptr>* occupyingActors = nullptr);
class ObstacleCheck
{
public:

View file

@ -10,6 +10,7 @@
#include <components/detournavigator/navigatorutils.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/math.hpp>
#include <components/misc/pathgridutils.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -38,7 +39,7 @@ namespace
// points to a quadtree may help
for (size_t counter = 0; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos);
float potentialDistBetween = Misc::distanceSquared(grid->mPoints[counter], pos);
if (potentialDistBetween < closestDistanceReachable)
{
// found a closer one
@ -197,7 +198,7 @@ namespace MWMechanics
// point right behind the wall that is closer than any pathgrid
// point outside the wall
osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint));
size_t startNode = getClosestPoint(pathgrid, startPointInLocalCoords);
const size_t startNode = Misc::getClosestPoint(*pathgrid, startPointInLocalCoords);
osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint));
std::pair<size_t, bool> endNode
@ -206,8 +207,8 @@ namespace MWMechanics
// if it's shorter for actor to travel from start to end, than to travel from either
// start or end to nearest pathgrid point, just travel from start to end.
float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2();
float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2))
{
*out++ = endPoint;
@ -223,7 +224,7 @@ namespace MWMechanics
{
ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]);
converter.toWorld(temp);
*out++ = makeOsgVec3(temp);
*out++ = Misc::Convert::makeOsgVec3f(temp);
}
else
{
@ -234,8 +235,8 @@ namespace MWMechanics
if (path.size() > 1)
{
ESM::Pathgrid::Point secondNode = *(++path.begin());
osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode);
osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode);
osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f;
osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f;
if (toSecondNodeVec3f * toStartPointVec3f > 0)
@ -259,7 +260,7 @@ namespace MWMechanics
// convert supplied path to world coordinates
std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) {
converter.toWorld(point);
return makeOsgVec3(point);
return Misc::Convert::makeOsgVec3f(point);
});
}
@ -359,26 +360,16 @@ namespace MWMechanics
mConstructed = true;
}
void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph)
{
mPath.clear();
mCell = cell;
buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath));
mConstructed = !mPath.empty();
}
void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType)
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType,
std::span<const osg::Vec3f> checkpoints)
{
mPath.clear();
// If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path
DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags,
areaCosts, endTolerance, pathType, std::back_inserter(mPath));
areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success)
mPath.clear();
@ -390,19 +381,19 @@ namespace MWMechanics
}
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType)
const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::span<const osg::Vec3f> checkpoints)
{
mPath.clear();
mCell = cell;
mCell = actor.getCell();
DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound;
if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor))
{
status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance,
pathType, std::back_inserter(mPath));
pathType, checkpoints, std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success)
mPath.clear();
}
@ -411,7 +402,7 @@ namespace MWMechanics
&& (flags & DetourNavigator::Flag_usePathgrid) == 0)
{
status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds,
flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType,
flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, checkpoints,
std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success)
mPath.clear();
@ -429,12 +420,13 @@ namespace MWMechanics
DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::back_insert_iterator<std::deque<osg::Vec3f>> out)
PathType pathType, std::span<const osg::Vec3f> checkpoints,
std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{
const auto world = MWBase::Environment::get().getWorld();
const auto navigator = world->getNavigator();
const auto status = DetourNavigator::findPath(
*navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out);
const MWBase::World& world = *MWBase::Environment::get().getWorld();
const DetourNavigator::Navigator& navigator = *world.getNavigator();
const DetourNavigator::Status status = DetourNavigator::findPath(
navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out);
if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath)
return DetourNavigator::Status::Success;
@ -451,9 +443,9 @@ namespace MWMechanics
}
void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType)
const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType)
{
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto maxDistance
@ -461,9 +453,9 @@ namespace MWMechanics
const auto startToEnd = endPoint - startPoint;
const auto distance = startToEnd.length();
if (distance <= maxDistance)
return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts,
endTolerance, pathType);
return buildPath(
actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
const auto end = startPoint + startToEnd * maxDistance / distance;
buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
buildPath(actor, startPoint, end, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
}
}

View file

@ -4,12 +4,13 @@
#include <cassert>
#include <deque>
#include <iterator>
#include <span>
#include <osg/Vec3f>
#include <components/detournavigator/areatype.hpp>
#include <components/detournavigator/flags.hpp>
#include <components/detournavigator/status.hpp>
#include <components/esm/position.hpp>
#include <components/esm3/loadpgrd.hpp>
namespace MWWorld
{
@ -102,23 +103,20 @@ namespace MWMechanics
void buildStraightPath(const osg::Vec3f& endPoint);
void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType);
PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType);
const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType);
const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType);
/// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance,
@ -145,61 +143,6 @@ namespace MWMechanics
mPath.push_back(point);
}
/// utility function to convert a osg::Vec3f to a Pathgrid::Point
static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v)
{
return ESM::Pathgrid::Point(static_cast<int>(v[0]), static_cast<int>(v[1]), static_cast<int>(v[2]));
}
/// utility function to convert an ESM::Position to a Pathgrid::Point
static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p)
{
return ESM::Pathgrid::Point(
static_cast<int>(p.pos[0]), static_cast<int>(p.pos[1]), static_cast<int>(p.pos[2]));
}
static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p)
{
return osg::Vec3f(static_cast<float>(p.mX), static_cast<float>(p.mY), static_cast<float>(p.mZ));
}
// Slightly cheaper version for comparisons.
// Caller needs to be careful for very short distances (i.e. less than 1)
// or when accumuating the results i.e. (a + b)^2 != a^2 + b^2
//
static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos)
{
return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position
// coordinates. NOTE: Does not check if there is a sensible way to get there
// (e.g. a cliff in front).
//
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
{
assert(grid && !grid->mPoints.empty());
float distanceBetween = distanceSquared(grid->mPoints[0], pos);
size_t closestIndex = 0;
// TODO: if this full scan causes performance problems mapping pathgrid
// points to a quadtree may help
for (size_t counter = 1; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos);
if (potentialDistBetween < distanceBetween)
{
distanceBetween = potentialDistBetween;
closestIndex = counter;
}
}
return closestIndex;
}
private:
bool mConstructed = false;
std::deque<osg::Vec3f> mPath;
@ -211,7 +154,8 @@ namespace MWMechanics
[[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::back_insert_iterator<std::deque<osg::Vec3f>> out);
PathType pathType, std::span<const osg::Vec3f> checkpoints,
std::back_insert_iterator<std::deque<osg::Vec3f>> out);
};
}

View file

@ -215,10 +215,6 @@ namespace MWMechanics
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f;
bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
effect.mDuration = std::max(1.f, effect.mDuration);
effect.mTimeLeft = effect.mDuration;
// add to list of active effects, to apply in next frame

View file

@ -377,8 +377,11 @@ namespace
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
return MWMechanics::MagicApplicationResult::Type::REMOVED;
}
effect.mMinMagnitude *= magnitudeMult;
effect.mMaxMagnitude *= magnitudeMult;
else if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{
effect.mMinMagnitude *= magnitudeMult;
effect.mMaxMagnitude *= magnitudeMult;
}
}
return MWMechanics::MagicApplicationResult::Type::APPLIED;
}
@ -1011,11 +1014,13 @@ namespace MWMechanics
else
{
// Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats
// updated instantly. We don't want to teleport instantly though
// updated instantly. We don't want to teleport instantly though. Nor do we want to force players to drink
// invisibility potions in the "right" order
if (!dt
&& (effect.mEffectId == ESM::MagicEffect::Recall
|| effect.mEffectId == ESM::MagicEffect::DivineIntervention
|| effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention))
|| effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention
|| effect.mEffectId == ESM::MagicEffect::Invisibility))
return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth };
auto& stats = target.getClass().getCreatureStats(target);
auto& magnitudes = stats.getMagicEffects();

View file

@ -10,7 +10,7 @@
namespace MWPhysics
{
// https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection
bool testAabbAgainstSphere(
inline bool testAabbAgainstSphere(
const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius)
{
const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()),
@ -18,35 +18,28 @@ namespace MWPhysics
return nearest.distance(position) < radius;
}
template <class Ignore, class OnCollision>
class HasSphereCollisionCallback final : public btBroadphaseAabbCallback
{
public:
HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group,
const Ignore& ignore, OnCollision* onCollision)
explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask,
const int group, const btCollisionObject* ignore)
: mPosition(position)
, mRadius(radius)
, mIgnore(ignore)
, mCollisionFilterMask(mask)
, mCollisionFilterGroup(group)
, mOnCollision(onCollision)
{
}
bool process(const btBroadphaseProxy* proxy) override
{
if (mResult && mOnCollision == nullptr)
if (mResult)
return false;
const auto collisionObject = static_cast<btCollisionObject*>(proxy->m_clientObject);
if (mIgnore(collisionObject) || !needsCollision(*proxy)
if (mIgnore == collisionObject || !needsCollision(*proxy)
|| !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius))
return true;
mResult = true;
if (mOnCollision != nullptr)
{
(*mOnCollision)(collisionObject);
return true;
}
return !mResult;
}
@ -55,10 +48,9 @@ namespace MWPhysics
private:
btVector3 mPosition;
btScalar mRadius;
Ignore mIgnore;
const btCollisionObject* mIgnore;
int mCollisionFilterMask;
int mCollisionFilterGroup;
OnCollision* mOnCollision;
bool mResult = false;
bool needsCollision(const btBroadphaseProxy& proxy) const

View file

@ -849,36 +849,18 @@ namespace MWPhysics
mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile);
}
bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const
bool PhysicsSystem::isAreaOccupiedByOtherActor(
const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const
{
std::vector<const btCollisionObject*> ignoredObjects;
ignoredObjects.reserve(ignore.size());
for (const auto& v : ignore)
if (const auto it = mActors.find(v.mRef); it != mActors.end())
ignoredObjects.push_back(it->second->getCollisionObject());
std::sort(ignoredObjects.begin(), ignoredObjects.end());
ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end());
const auto ignoreFilter = [&](const btCollisionObject* v) {
return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v);
};
const auto bulletPosition = Misc::Convert::toBullet(position);
const auto aabbMin = bulletPosition - btVector3(radius, radius, radius);
const auto aabbMax = bulletPosition + btVector3(radius, radius, radius);
const btCollisionObject* ignoredObject = nullptr;
if (const auto it = mActors.find(actor); it != mActors.end())
ignoredObject = it->second->getCollisionObject();
const btVector3 bulletPosition = Misc::Convert::toBullet(position);
const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius);
const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius);
const int mask = MWPhysics::CollisionType_Actor;
const int group = MWPhysics::CollisionType_AnyPhysical;
if (occupyingActors == nullptr)
{
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter,
static_cast<void (*)(const btCollisionObject*)>(nullptr));
mTaskScheduler->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult();
}
const auto onCollision = [&](const btCollisionObject* object) {
if (PtrHolder* holder = static_cast<PtrHolder*>(object->getUserPointer()))
occupyingActors->push_back(holder->getPtr());
};
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision);
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject);
mTaskScheduler->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult();
}

View file

@ -281,8 +281,8 @@ namespace MWPhysics
std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function);
}
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const;
bool isAreaOccupiedByOtherActor(
const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const;
void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
void reportCollision(const btVector3& position, const btVector3& normal);

View file

@ -566,10 +566,10 @@ namespace MWWorld
std::vector<Ref> refs;
std::set<ESM::RefId> keyIDs;
std::vector<ESM::RefId> refIDs;
Store<ESM::Cell> Cells = get<ESM::Cell>();
for (auto it = Cells.intBegin(); it != Cells.intEnd(); ++it)
const Store<ESM::Cell>& cells = get<ESM::Cell>();
for (auto it = cells.intBegin(); it != cells.intEnd(); ++it)
readRefs(*it, refs, refIDs, keyIDs, readers);
for (auto it = Cells.extBegin(); it != Cells.extEnd(); ++it)
for (auto it = cells.extBegin(); it != cells.extEnd(); ++it)
readRefs(*it, refs, refIDs, keyIDs, readers);
const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; };
std::stable_sort(refs.begin(), refs.end(), lessByRefNum);

View file

@ -3871,10 +3871,11 @@ namespace MWWorld
return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal);
}
bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const
bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const
{
return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors);
const osg::Vec3f halfExtents = getPathfindingAgentBounds(actor).mHalfExtents;
const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
return mPhysics->isAreaOccupiedByOtherActor(actor.mRef, position, 2 * maxHalfExtent);
}
void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const

View file

@ -664,8 +664,7 @@ namespace MWWorld
bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override;
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const override;
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override;
void reportStats(unsigned int frameNumber, osg::Stats& stats) const override;

View file

@ -7,6 +7,7 @@
#include <components/esm3/loadnpc.hpp>
#include <components/esm3/readerscache.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace MWWorld
@ -36,7 +37,7 @@ namespace MWWorld
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
liveCellRef.mData.setDeletedByContentFile(true);
Ptr ptr(&liveCellRef);
EXPECT_EQ(ptr.toString(), "deleted object0xd00002a (NPC, \"player\")");
EXPECT_THAT(ptr.toString(), StrCaseEq("deleted object0xd00002a (NPC, \"player\")"));
}
TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr)
@ -53,7 +54,7 @@ namespace MWWorld
cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd };
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
Ptr ptr(&liveCellRef);
EXPECT_EQ(ptr.toString(), "object0xd00002a (NPC, \"player\")");
EXPECT_THAT(ptr.toString(), StrCaseEq("object0xd00002a (NPC, \"player\")"));
}
TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction)

View file

@ -13,7 +13,6 @@
#include <osg/Vec3f>
#include <array>
#include <cassert>
#include <functional>
#include <iterator>
@ -64,6 +63,25 @@ namespace DetourNavigator
std::reference_wrapper<const RecastSettings> mSettings;
};
template <class T>
class ToNavMeshCoordinatesSpan
{
public:
explicit ToNavMeshCoordinatesSpan(std::span<T> span, const RecastSettings& settings)
: mSpan(span)
, mSettings(settings)
{
}
std::size_t size() const noexcept { return mSpan.size(); }
T operator[](std::size_t i) const noexcept { return toNavMeshCoordinates(mSettings, mSpan[i]); }
private:
std::span<T> mSpan;
const RecastSettings& mSettings;
};
inline std::optional<std::size_t> findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef,
const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter,
std::span<dtPolyRef> pathBuffer)
@ -79,7 +97,7 @@ namespace DetourNavigator
}
Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end,
std::span<dtPolyRef> polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize,
std::span<dtPolyRef> polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst,
std::output_iterator<osg::Vec3f> auto& out)
{
assert(polygonPathSize <= polygonPath.size());
@ -95,7 +113,7 @@ namespace DetourNavigator
dtStatusFailed(status))
return Status::FindStraightPathFailed;
for (int i = 0; i < cornersCount; ++i)
for (int i = skipFirst ? 1 : 0; i < cornersCount; ++i)
*out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast<std::size_t>(i) * 3]);
return Status::Success;
@ -103,7 +121,8 @@ namespace DetourNavigator
Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start,
const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings,
float endTolerance, std::output_iterator<osg::Vec3f> auto out)
float endTolerance, const ToNavMeshCoordinatesSpan<const osg::Vec3f>& checkpoints,
std::output_iterator<osg::Vec3f> auto out)
{
dtQueryFilter queryFilter;
queryFilter.setIncludeFlags(includeFlags);
@ -131,29 +150,66 @@ namespace DetourNavigator
return Status::EndPolygonNotFound;
std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize);
const auto polygonPathSize
= findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath);
std::span<dtPolyRef> polygonPathBuffer = polygonPath;
dtPolyRef currentRef = startRef;
osg::Vec3f currentNavMeshPos = startNavMeshPos;
bool skipFirst = false;
if (!polygonPathSize.has_value())
for (std::size_t i = 0; i < checkpoints.size(); ++i)
{
const osg::Vec3f checkpointPos = checkpoints[i];
osg::Vec3f checkpointNavMeshPos;
dtPolyRef checkpointRef;
if (const dtStatus status = navMeshQuery.findNearestPoly(checkpointPos.ptr(), polyHalfExtents.ptr(),
&queryFilter, &checkpointRef, checkpointNavMeshPos.ptr());
dtStatusFailed(status) || checkpointRef == 0)
continue;
const std::optional<std::size_t> toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef,
checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath);
if (!toCheckpointPathSize.has_value())
continue;
if (*toCheckpointPathSize == 0)
continue;
if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef)
continue;
const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos,
polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
if (smoothStatus != Status::Success)
return smoothStatus;
currentRef = checkpointRef;
currentNavMeshPos = checkpointNavMeshPos;
skipFirst = true;
}
const std::optional<std::size_t> toEndPathSize = findPolygonPath(
navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer);
if (!toEndPathSize.has_value())
return Status::FindPathOverPolygonsFailed;
if (*polygonPathSize == 0)
return Status::Success;
if (*toEndPathSize == 0)
return currentRef == endRef ? Status::Success : Status::PartialPath;
osg::Vec3f targetNavMeshPos;
if (const dtStatus status = navMeshQuery.closestPointOnPoly(
polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
dtStatusFailed(status))
return Status::TargetPolygonNotFound;
const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef;
const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath,
*polygonPathSize, settings.mMaxSmoothPathSize, out);
const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath,
*toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
if (smoothStatus != Status::Success)
return smoothStatus;
return partialPath ? Status::PartialPath : Status::Success;
return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath;
}
}

View file

@ -11,6 +11,7 @@
#include <iterator>
#include <optional>
#include <span>
namespace DetourNavigator
{
@ -21,13 +22,13 @@ namespace DetourNavigator
* @param end path at given point.
* @param includeFlags setup allowed navmesh areas.
* @param out the beginning of the destination range.
* @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents
* @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents.
* @param checkpoints is a sequence of positions the path should go over if possible.
* @return Status.
* Equal to out if no path is found.
*/
inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start,
const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance,
std::output_iterator<osg::Vec3f> auto out)
std::span<const osg::Vec3f> checkpoints, std::output_iterator<osg::Vec3f> auto out)
{
const auto navMesh = navigator.getNavMesh(agentBounds);
if (navMesh == nullptr)
@ -37,7 +38,8 @@ namespace DetourNavigator
const auto locked = navMesh->lock();
return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents),
toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags,
areaCosts, settings.mDetour, endTolerance, outTransform);
areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast),
outTransform);
}
/**

View file

@ -15,7 +15,9 @@ namespace ESMTerrain
inline std::pair<std::size_t, std::size_t> toCellAndLocal(
std::size_t begin, std::size_t global, std::size_t cellSize)
{
// NOLINTBEGIN(clang-analyzer-core.UndefinedBinaryOperatorResult)
std::size_t cell = global / (cellSize - 1);
// NOLINTEND(clang-analyzer-core.UndefinedBinaryOperatorResult)
std::size_t local = global & (cellSize - 2);
if (global != begin && local == 0)
{

View file

@ -59,10 +59,18 @@ namespace Misc
point.y() -= static_cast<float>(mCellY);
}
osg::Vec3f toWorldVec3(const osg::Vec3f& point) const
{
osg::Vec3f result = point;
toWorld(result);
return result;
}
osg::Vec3f toLocalVec3(const osg::Vec3f& point) const
{
return osg::Vec3f(
point.x() - static_cast<float>(mCellX), point.y() - static_cast<float>(mCellY), point.z());
osg::Vec3f result = point;
toLocal(result);
return result;
}
private:

View file

@ -0,0 +1,53 @@
#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H
#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H
#include "convert.hpp"
#include <components/esm3/loadpgrd.hpp>
#include <osg/Vec3f>
#include <stdexcept>
namespace Misc
{
// Slightly cheaper version for comparisons.
// Caller needs to be careful for very short distances (i.e. less than 1)
// or when accumuating the results i.e. (a + b)^2 != a^2 + b^2
//
inline float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos)
{
return (Misc::Convert::makeOsgVec3f(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position
// coordinates. NOTE: Does not check if there is a sensible way to get there
// (e.g. a cliff in front).
//
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
inline std::size_t getClosestPoint(const ESM::Pathgrid& grid, const osg::Vec3f& pos)
{
if (grid.mPoints.empty())
throw std::invalid_argument("Pathgrid has no points");
float minDistance = distanceSquared(grid.mPoints[0], pos);
std::size_t closestIndex = 0;
// TODO: if this full scan causes performance problems mapping pathgrid
// points to a quadtree may help
for (std::size_t i = 1; i < grid.mPoints.size(); ++i)
{
const float distance = distanceSquared(grid.mPoints[i], pos);
if (minDistance > distance)
{
minDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
}
#endif

View file

@ -229,7 +229,7 @@
-- * `boneName` - name of the bone to attach the vfx to. (default: "")
-- * `particleTextureOverride` - name of the particle texture to use. (default: "")
-- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: "").
-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true)
-- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true)
--
-- @usage local mgef = core.magic.effects.records[myEffectName]
-- anim.addVfx(self, 'VFX_Hands', {boneName = 'Bip01 L Hand', particleTextureOverride = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'})

View file

@ -180,6 +180,7 @@
-- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area.
-- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed
-- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1).
-- @field [parent=#FindPathOptions] #table checkpoints an array of positions to build path over if possible.
---
-- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay}

View file

@ -195,7 +195,7 @@
-- * `mwMagicVfx` - Boolean that if true causes the textureOverride parameter to only affect nodes with the Nif::RC_NiTexturingProperty property set. (default: true).
-- * `particleTextureOverride` - Name of a particle texture that should override this effect's default texture. (default: "")
-- * `scale` - A number that scales the size of the vfx (Default: 1)
-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: 1)
-- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true)
--
-- @usage -- Spawn a sanctuary effect near the player
-- local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.Sanctuary]

View file

@ -315,6 +315,7 @@ registerPlayerTest('player rotation')
registerPlayerTest('player forward running')
registerPlayerTest('player diagonal walking')
registerPlayerTest('findPath')
registerPlayerTest('findPath with checkpoints')
registerPlayerTest('findRandomPointAroundCircle')
registerPlayerTest('castNavigationRay')
registerPlayerTest('findNearestNavMeshPosition')

View file

@ -82,6 +82,7 @@ registerGlobalTest('player rotation', 'rotating player should not lead to nan ro
registerGlobalTest('player forward running')
registerGlobalTest('player diagonal walking')
registerGlobalTest('findPath')
registerGlobalTest('findPath with checkpoints')
registerGlobalTest('findRandomPointAroundCircle')
registerGlobalTest('castNavigationRay')
registerGlobalTest('findNearestNavMeshPosition')

View file

@ -179,6 +179,39 @@ testing.registerLocalTest('findPath',
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status')
testing.expectLessOrEqual((path[#path] - dst):length(), 1,
'Last path point ' .. testing.formatActualExpected(path[#path], dst))
testing.expectThat(path, matchers.equalTo({
matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1),
matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1),
}))
end)
testing.registerLocalTest('findPath with checkpoints',
function()
local src = util.vector3(4096, 4096, 1745)
local dst = util.vector3(4500, 4500, 1745.95263671875)
local options = {
agentBounds = types.Actor.getPathfindingAgentBounds(self),
includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim,
areaCosts = {
water = 1,
door = 2,
ground = 1,
pathgrid = 1,
},
destinationTolerance = 1,
checkpoints = {
util.vector3(4200, 4100, 1750),
},
}
local status, path = nearby.findPath(src, dst, options)
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status')
testing.expectLessOrEqual((path[#path] - dst):length(), 1,
'Last path point ' .. testing.formatActualExpected(path[#path], dst))
testing.expectThat(path, matchers.equalTo({
matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1),
matchers.closeToVector(util.vector3(4200, 4100, 1749.5076904296875), 1e-1),
matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1),
}))
end)
testing.registerLocalTest('findRandomPointAroundCircle',