aipackage.cpp 14.4 KB
Newer Older
1 2
#include "aipackage.hpp"

3
#include <cmath>
scrawl's avatar
scrawl committed
4 5 6

#include <components/esm/loadcell.hpp>
#include <components/esm/loadland.hpp>
7
#include <components/esm/loadmgef.hpp>
8
#include <components/detournavigator/navigator.hpp>
scrawl's avatar
scrawl committed
9

10 11
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
12 13

#include "../mwworld/action.hpp"
14 15
#include "../mwworld/class.hpp"
#include "../mwworld/cellstore.hpp"
16
#include "../mwworld/inventorystore.hpp"
17

18
#include "pathgrid.hpp"
19 20 21
#include "creaturestats.hpp"
#include "movement.hpp"
#include "steering.hpp"
22
#include "actorutil.hpp"
23
#include "coordinateconverter.hpp"
24

25 26
#include <osg/Quat>

27
MWMechanics::AiPackage::~AiPackage() {}
28

29
MWMechanics::AiPackage::AiPackage() :
30
    mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild
31 32
    mTargetActorRefId(""),
    mTargetActorId(-1),
scrawl's avatar
scrawl committed
33
    mRotateOnTheRunChecks(0),
34
    mIsShortcutting(false),
35 36
    mShortcutProhibited(false),
    mShortcutFailPos()
37 38 39
{
}

scrawl's avatar
scrawl committed
40
MWWorld::Ptr MWMechanics::AiPackage::getTarget() const
41
{
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
    if (mTargetActorId == -2)
        return MWWorld::Ptr();

    if (mTargetActorId == -1)
    {
        MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false);
        if (target.isEmpty())
        {
            mTargetActorId = -2;
            return target;
        }
        else
            mTargetActorId = target.getClass().getCreatureStats(target).getActorId();
    }

    if (mTargetActorId != -1)
        return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId);
    else
        return MWWorld::Ptr();
61 62 63 64 65 66 67
}

bool MWMechanics::AiPackage::sideWithTarget() const
{
    return false;
}

68 69 70 71 72
bool MWMechanics::AiPackage::followTargetThroughDoors() const
{
    return false;
}

73 74 75 76 77 78 79 80 81 82
bool MWMechanics::AiPackage::canCancel() const
{
    return true;
}

bool MWMechanics::AiPackage::shouldCancelPreviousAi() const
{
    return true;
}

83 84
bool MWMechanics::AiPackage::getRepeat() const
{
85
    return false;
86 87
}

88 89 90 91 92 93
void MWMechanics::AiPackage::reset()
{
    // reset all members
    mTimer = AI_REACTION_TIME + 1.0f;
    mIsShortcutting = false;
    mShortcutProhibited = false;
94
    mShortcutFailPos = osg::Vec3f();
mrcheko's avatar
mrcheko committed
95

96 97
    mPathFinder.clearPath();
    mObstacleCheck.clear();
mrcheko's avatar
mrcheko committed
98 99
}

100
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance)
101
{
102
    mTimer += duration; //Update timer
103

elsid's avatar
elsid committed
104
    const auto position = actor.getRefData().getPosition().asVec3(); //position of the actor
105
    const auto world = MWBase::Environment::get().getWorld();
106

107
    {
108 109
        const auto halfExtents = world->getHalfExtents(actor);
        world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest);
110 111
    }

112
    /// Stops the actor when it gets too close to a unloaded cell
113
    //... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value
114 115
    //... units from player, and exterior cells are 8192 units long and wide.
    //... But AI processing distance may increase in the future.
elsid's avatar
elsid committed
116
    if (isNearInactiveCell(position))
117
    {
118 119
        actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
        return false;
120 121
    }

elsid's avatar
elsid committed
122
    const float distToTarget = distance(position, dest);
123
    const bool isDestReached = (distToTarget <= destTolerance);
124 125

    if (!isDestReached && mTimer > AI_REACTION_TIME)
126
    {
127 128 129
        if (actor.getClass().isBipedal(actor))
            openDoors(actor);

130
        const bool wasShortcutting = mIsShortcutting;
131
        bool destInLOS = false;
132
        const bool actorCanMoveByZ = canActorMoveByZAxis(actor);
133 134

        // Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions.
135 136
        mIsShortcutting = actorCanMoveByZ
            && shortcutPath(position, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first
137

138
        if (!mIsShortcutting)
139
        {
140
            if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path
141
            {
elsid's avatar
elsid committed
142
                const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
143
                mPathFinder.buildPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()),
elsid's avatar
elsid committed
144
                    playerHalfExtents, getNavigatorFlags(actor));
145
                mRotateOnTheRunChecks = 3;
146 147 148 149 150

                // give priority to go directly on target if there is minimal opportunity
                if (destInLOS && mPathFinder.getPath().size() > 1)
                {
                    // get point just before dest
elsid's avatar
elsid committed
151
                    auto pPointBeforeDest = mPathFinder.getPath().rbegin() + 1;
152 153

                    // if start point is closer to the target then last point of path (excluding target itself) then go straight on the target
elsid's avatar
elsid committed
154
                    if (distance(position, dest) <= distance(dest, *pPointBeforeDest))
155 156 157 158 159 160
                    {
                        mPathFinder.clearPath();
                        mPathFinder.addPointToPath(dest);
                    }
                }
            }
161

162 163
            if (!mPathFinder.getPath().empty()) //Path has points in it
            {
164
                const auto& lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path
165 166 167 168

                if(distance(dest, lastPos) > 100) //End of the path is far from the destination
                    mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go
            }
169 170 171 172 173
        }

        mTimer = 0;
    }

174 175 176
    const auto pointTolerance = std::min(actor.getClass().getSpeed(actor), DEFAULT_TOLERANCE);

    mPathFinder.update(position, pointTolerance, destTolerance);
177 178

    if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished
179
    {
180
        // turn to destination point
elsid's avatar
elsid committed
181 182
        zTurn(actor, getZAngleToPoint(position, dest));
        smoothTurn(actor, getXAngleToPoint(position, dest), 0);
183
        return true;
184
    }
elsid's avatar
elsid committed
185 186 187

    if (mRotateOnTheRunChecks == 0
        || isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point
188
    {
elsid's avatar
elsid committed
189 190 191
        actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target
        if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--;
    }
192

193
    // turn to next path point by X,Z axes
elsid's avatar
elsid committed
194 195
    zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y()));
    smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0);
196

elsid's avatar
elsid committed
197
    mObstacleCheck.update(actor, duration);
198

elsid's avatar
elsid committed
199
    // handle obstacles on the way
elsid's avatar
elsid committed
200
    evadeObstacles(actor);
201

202
    return false;
203
}
204

elsid's avatar
elsid committed
205
void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor)
206
{
207
    // check if stuck due to obstacles
208
    if (!mObstacleCheck.isEvading()) return;
209 210

    // first check if obstacle is a door
211 212
    static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();

213 214
    const MWWorld::Ptr door = getNearbyDoor(actor, distance);
    if (!door.isEmpty() && actor.getClass().isBipedal(actor))
215
    {
216
        openDoors(actor);
217
    }
218
    else
219
    {
elsid's avatar
elsid committed
220
        mObstacleCheck.takeEvasiveAction(actor.getClass().getMovementSettings(actor));
221 222 223
    }
}

224 225
void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
{
226 227
    const auto world = MWBase::Environment::get().getWorld();
    static float distance = world->getMaxActivationDistance();
228 229 230 231 232 233 234 235 236 237

    const MWWorld::Ptr door = getNearbyDoor(actor, distance);
    if (door == MWWorld::Ptr())
        return;

    // note: AiWander currently does not open doors
    if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == 0)
    {
        if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 ))
        {
238
            world->activate(door, actor);
239 240 241 242 243 244 245 246 247 248 249
            return;
        }

        const std::string keyId = door.getCellRef().getKey();
        if (keyId.empty())
            return;

        MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor);
        MWWorld::Ptr keyPtr = invStore.search(keyId);

        if (!keyPtr.isEmpty())
250
            world->activate(door, actor);
251 252 253
    }
}

254 255 256 257 258 259 260 261 262 263 264 265 266 267
const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const MWWorld::CellStore *cell)
{
    const ESM::CellId& id = cell->getCell()->getCellId();
    // static cache is OK for now, pathgrids can never change during runtime
    typedef std::map<ESM::CellId, std::unique_ptr<MWMechanics::PathgridGraph> > CacheMap;
    static CacheMap cache;
    CacheMap::iterator found = cache.find(id);
    if (found == cache.end())
    {
        cache.insert(std::make_pair(id, std::unique_ptr<MWMechanics::PathgridGraph>(new MWMechanics::PathgridGraph(cell))));
    }
    return *cache[id].get();
}

268 269
bool MWMechanics::AiPackage::shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
        const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear)
270
{
271
    if (!mShortcutProhibited || (mShortcutFailPos - startPoint).length() >= PATHFIND_SHORTCUT_RETRY_DIST)
272 273 274
    {
        // check if target is clearly visible
        isPathClear = !MWBase::Environment::get().getWorld()->castRay(
275 276
            startPoint.x(), startPoint.y(), startPoint.z(),
            endPoint.x(), endPoint.y(), endPoint.z());
277

278
        if (destInLOS != nullptr) *destInLOS = isPathClear;
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296

        if (!isPathClear)
            return false;

        // check if an actor can move along the shortcut path
        isPathClear = checkWayIsClearForActor(startPoint, endPoint, actor);
    }

    if (isPathClear) // can shortcut the path
    {
        mPathFinder.clearPath();
        mPathFinder.addPointToPath(endPoint);
        return true;
    }

    return false;
}

297
bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor)
298
{
299 300 301
    const auto world = MWBase::Environment::get().getWorld();
    const bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && world->isSwimming(actor))
        || world->isFlying(actor);
302 303 304 305

    if (actorCanMoveByZ)
        return true;

306 307 308
    const float actorSpeed = actor.getClass().getSpeed(actor);
    const float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
    const float distToTarget = osg::Vec2f(endPoint.x(), endPoint.y()).length();
309

310
    const float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2;
311

312
    const bool isClear = checkWayIsClear(startPoint, endPoint, offsetXY);
313 314 315 316 317

    // update shortcut prohibit state
    if (isClear)
    {
        if (mShortcutProhibited)
318
        {
319
            mShortcutProhibited = false;
320
            mShortcutFailPos = osg::Vec3f();
321 322
        }
    }
323 324
    if (!isClear)
    {
325
        if (mShortcutFailPos == osg::Vec3f())
326 327 328 329
        {
            mShortcutProhibited = true;
            mShortcutFailPos = startPoint;
        }
330
    }
331 332

    return isClear;
333 334
}

335
bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell)
336
{
337 338 339
    return mPathFinder.getPath().empty()
        || (distance(mPathFinder.getPath().back(), newDest) > 10)
        || mPathFinder.getPathCell() != currentCell;
340
}
341 342 343

bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
{
dteviot's avatar
dteviot committed
344
    const MagicEffects& magicEffects(target.getClass().getCreatureStats(target).getMagicEffects());
345 346 347
    return (magicEffects.get(ESM::MagicEffect::Invisibility).getMagnitude() > 0)
        || (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75);
}
348

elsid's avatar
elsid committed
349
bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position)
350 351 352 353 354
{
    const ESM::Cell* playerCell(getPlayer().getCell()->getCell());
    if (playerCell->isExterior())
    {
        // get actor's distance from origin of center cell
elsid's avatar
elsid committed
355
        CoordinateConverter(playerCell).toLocal(position);
356 357 358

        // currently assumes 3 x 3 grid for exterior cells, with player at center cell.
        // ToDo: (Maybe) use "exterior cell load distance" setting to get count of actual active cells
359
        // AI shuts down actors before they reach edges of 3 x 3 grid.
360 361 362
        const float distanceFromEdge = 200.0;
        float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge;
        float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge;
elsid's avatar
elsid committed
363 364
        return (position.x() < minThreshold) || (maxThreshold < position.x())
            || (position.y() < minThreshold) || (maxThreshold < position.y());
365 366 367 368 369
    }
    else
    {
        return false;
    }
370
}
371

372
bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest)
373 374 375 376 377 378 379 380 381 382 383 384 385
{
    // get actor's shortest radius for moving in circle
    float speed = actor.getClass().getSpeed(actor);
    speed += speed * 0.1f; // 10% real speed inaccuracy
    float radius = speed / MAX_VEL_ANGULAR_RADIANS;

    // get radius direction to the center
    const float* rot = actor.getRefData().getPosition().rot;
    osg::Quat quatRot(rot[0], -osg::X_AXIS, rot[1], -osg::Y_AXIS, rot[2], -osg::Z_AXIS);
    osg::Vec3f dir = quatRot * osg::Y_AXIS; // actor's orientation direction is a tangent to circle
    osg::Vec3f radiusDir = dir ^ osg::Z_AXIS; // radius is perpendicular to a tangent
    radiusDir.normalize();
    radiusDir *= radius;
386

387 388 389 390
    // pick up the nearest center candidate
    osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
    osg::Vec3f center1 = pos - radiusDir;
    osg::Vec3f center2 = pos + radiusDir;
391
    osg::Vec3f center = (center1 - dest).length2() < (center2 - dest).length2() ? center1 : center2;
392

393
    float distToDest = (center - dest).length();
394 395 396 397 398

    // if pathpoint is reachable for the actor rotating on the run:
    // no points of actor's circle should be farther from the center than destination point
    return (radius <= distToDest);
}
399 400 401 402 403 404 405 406 407 408 409 410

DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const
{
    const auto& actorClass = actor.getClass();
    DetourNavigator::Flags result = DetourNavigator::Flag_none;

    if (actorClass.isPureWaterCreature(actor) || (getTypeId() != TypeIdWander && actorClass.canSwim(actor)))
        result |= DetourNavigator::Flag_swim;

    if (actorClass.canWalk(actor))
        result |= DetourNavigator::Flag_walk;

411 412 413
    if (actorClass.isBipedal(actor) && getTypeId() != TypeIdWander)
        result |= DetourNavigator::Flag_openDoor;

414 415
    return result;
}
416 417 418 419 420 421 422

bool MWMechanics::AiPackage::canActorMoveByZAxis(const MWWorld::Ptr& actor) const
{
    const auto world = MWBase::Environment::get().getWorld();
    const auto& actorClass = actor.getClass();
    return (actorClass.canSwim(actor) && world->isSwimming(actor)) || world->isFlying(actor);
}