aicombat.cpp 28.6 KB
Newer Older
gus's avatar
gus committed
1 2
#include "aicombat.hpp"

scrawl's avatar
scrawl committed
3
#include <components/misc/rng.hpp>
4

scrawl's avatar
scrawl committed
5
#include <components/esm/aisequence.hpp>
gus's avatar
gus committed
6

7 8 9 10
#include <components/sceneutil/positionattitudetransform.hpp>

#include "../mwphysics/collisiontype.hpp"

gus's avatar
gus committed
11
#include "../mwworld/class.hpp"
12
#include "../mwworld/esmstore.hpp"
mrcheko's avatar
mrcheko committed
13

gus's avatar
gus committed
14
#include "../mwbase/environment.hpp"
mrcheko's avatar
mrcheko committed
15
#include "../mwbase/dialoguemanager.hpp"
scrawl's avatar
scrawl committed
16
#include "../mwbase/mechanicsmanager.hpp"
mrcheko's avatar
mrcheko committed
17

18 19
#include "../mwrender/animation.hpp"

20
#include "pathgrid.hpp"
21
#include "creaturestats.hpp"
22 23
#include "steering.hpp"
#include "movement.hpp"
scrawl's avatar
scrawl committed
24
#include "character.hpp"
25
#include "aicombataction.hpp"
26
#include "combat.hpp"
27
#include "coordinateconverter.hpp"
28
#include "actorutil.hpp"
29

gus's avatar
gus committed
30 31
namespace
{
mrcheko's avatar
mrcheko committed
32 33

    //chooses an attack depending on probability to avoid uniformity
34
    std::string chooseBestAttack(const ESM::Weapon* weapon);
35

scrawl's avatar
scrawl committed
36
    osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos,
37
        float duration, int weapType, float strength);
gus's avatar
gus committed
38
}
gus's avatar
gus committed
39 40 41

namespace MWMechanics
{
42 43 44 45
    AiCombat::AiCombat(const MWWorld::Ptr& actor)
    {
        mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
    }
scrawl's avatar
scrawl committed
46

scrawl's avatar
scrawl committed
47 48 49 50 51
    AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat)
    {
        mTargetActorId = combat->mTargetActorId;
    }

scrawl's avatar
scrawl committed
52 53
    void AiCombat::init()
    {
54

gus's avatar
gus committed
55 56
    }

57 58 59 60
    /*
     * Current AiCombat movement states (as of 0.29.0), ignoring the details of the
     * attack states such as CombatMove, Strike and ReadyToAttack:
     *
cc9cii's avatar
cc9cii committed
61 62 63 64 65 66 67
     *    +----(within strike range)----->attack--(beyond strike range)-->follow
     *    |                                 | ^                            | |
     *    |                                 | |                            | |
     *  pursue<---(beyond follow range)-----+ +----(within strike range)---+ |
     *    ^                                                                  |
     *    |                                                                  |
     *    +-------------------------(beyond follow range)--------------------+
68 69
     *
     *
cc9cii's avatar
cc9cii committed
70 71
     * Below diagram is high level only, the code detail is a little different
     * (but including those detail will just complicate the diagram w/o adding much)
72
     *
cc9cii's avatar
cc9cii committed
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
     *    +----------(same)-------------->attack---------(same)---------->follow
     *    |                                 |^^                            |||
     *    |                                 |||                            |||
     *    |       +--(same)-----------------+|+----------(same)------------+||
     *    |       |                          |                              ||
     *    |       |                          | (in range)                   ||
     *    |   <---+         (too far)        |                              ||
     *  pursue<-------------------------[door open]<-----+                  ||
     *    ^^^                                            |                  ||
     *    |||                                            |                  ||
     *    ||+----------evade-----+                       |                  ||
     *    ||                     |    [closed door]      |                  ||
     *    |+----> maybe stuck, check --------------> back up, check door    ||
     *    |         ^   |   ^                          |   ^                ||
     *    |         |   |   |                          |   |                ||
     *    |         |   +---+                          +---+                ||
     *    |         +-------------------------------------------------------+|
     *    |                                                                  |
     *    +---------------------------(same)---------------------------------+
92 93 94 95 96 97 98 99
     *
     * FIXME:
     *
     * The new scheme is way too complicated, should really be implemented as a
     * proper state machine.
     *
     * TODO:
     *
100
     * Use the observer pattern to coordinate attacks, provide intelligence on
101 102
     * whether the target was hit, etc.
     */
103

104
    bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration)
gus's avatar
gus committed
105
    {
106
        // get or create temporary storage
terrorfisch's avatar
terrorfisch committed
107 108
        AiCombatStorage& storage = state.get<AiCombatStorage>();
        
mrcheko's avatar
mrcheko committed
109
        //General description
110
        if (actor.getClass().getCreatureStats(actor).isDead())
111
            return true;
gus's avatar
gus committed
112

113
        MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId);
114 115
        if (target.isEmpty())
            return false;
116

117 118 119
        if(!target.getRefData().getCount() || !target.getRefData().isEnabled()  // Really we should be checking whether the target is currently registered
                                                                                // with the MechanicsManager
                || target.getClass().getCreatureStats(target).isDead())
mrcheko's avatar
mrcheko committed
120
            return true;
121

122
        if (!storage.isFleeing())
123
        {
124
            if (storage.mCurrentAction.get()) // need to wait to init action with its attack range
125
            {
126 127 128 129 130
                //Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame.
                updateLOS(actor, target, duration, storage);
                float targetReachedTolerance = 0.0f;
                if (storage.mLOS)
                    targetReachedTolerance = storage.mAttackRange;
131
                const bool is_target_reached = pathTo(actor, target.getRefData().getPosition().asVec3(), duration, targetReachedTolerance);
132 133
                if (is_target_reached) storage.mReadyToAttack = true;
            }
134

135 136 137 138 139 140 141 142
            storage.updateCombatMove(duration);
            if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage);
            storage.updateAttack(characterController);
        }
        else
        {
            updateFleeing(actor, target, duration, storage);
        }
143
        storage.mActionCooldown -= duration;
144

145
        float& timerReact = storage.mTimerReact;
146
        if (timerReact < AI_REACTION_TIME)
mrcheko's avatar
mrcheko committed
147
        {
terrorfisch's avatar
terrorfisch committed
148
            timerReact += duration;
mrcheko's avatar
mrcheko committed
149
        }
150 151 152
        else
        {
            timerReact = 0;
153 154
            if (attack(actor, target, storage, characterController))
                return true;
155
        }
156 157

        return false;
158
    }
gus's avatar
gus committed
159

160
    bool AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController)
161
    {
162
        const MWWorld::CellStore*& currentCell = storage.mCell;
terrorfisch's avatar
terrorfisch committed
163 164
        bool cellChange = currentCell && (actor.getCell() != currentCell);
        if(!currentCell || cellChange)
cc9cii's avatar
cc9cii committed
165
        {
terrorfisch's avatar
terrorfisch committed
166
            currentCell = actor.getCell();
cc9cii's avatar
cc9cii committed
167 168
        }

169 170 171 172 173 174
        bool forceFlee = false;
        if (!canFight(actor, target))
        {
            storage.stopAttack();
            characterController.setAttackingOrSpell(false);
            storage.mActionCooldown = 0.f;
175 176 177 178
            // Continue combat if target is player or player follower/escorter and an attack has been attempted
            const std::list<MWWorld::Ptr>& playerFollowersAndEscorters = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer());
            bool targetSidesWithPlayer = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end());
            if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer)
179 180
                && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId())
                || (target.getClass().getCreatureStats(target).getHitAttemptActorId() == actor.getClass().getCreatureStats(actor).getActorId())))
181
                forceFlee = true;
182
            else // Otherwise end combat
183
                return true;
184 185
        }

186
        const MWWorld::Class& actorClass = actor.getClass();
187 188
        actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);

189
        float& actionCooldown = storage.mActionCooldown;
190
        std::shared_ptr<Action>& currentAction = storage.mCurrentAction;
191 192

        if (!forceFlee)
193
        {
194
            if (actionCooldown > 0)
195
                return false;
196 197 198 199 200 201 202 203 204 205

            if (characterController.readyToPrepareAttack())
            {
                currentAction = prepareNextAction(actor, target);
                actionCooldown = currentAction->getActionCooldown();
            }
        }
        else
        {
            currentAction.reset(new ActionFlee());
terrorfisch's avatar
terrorfisch committed
206
            actionCooldown = currentAction->getActionCooldown();
207
        }
208

209
        if (!currentAction)
210
            return false;
211 212

        if (storage.isFleeing() != currentAction->isFleeing())
213
        {
214 215 216 217
            if (currentAction->isFleeing())
            {
                storage.startFleeing();
                MWBase::Environment::get().getDialogueManager()->say(actor, "flee");
218
                return false;
219 220 221
            }
            else
                storage.stopFleeing();
222 223
        }

224 225 226 227 228 229 230 231
        bool isRangedCombat = false;
        float &rangeAttack = storage.mAttackRange;

        rangeAttack = currentAction->getCombatRange(isRangedCombat);

        // Get weapon characteristics
        const ESM::Weapon* weapon = currentAction->getWeapon();

mrcheko's avatar
mrcheko committed
232
        ESM::Position pos = actor.getRefData().getPosition();
scrawl's avatar
scrawl committed
233 234
        osg::Vec3f vActorPos(pos.asVec3());
        osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3());
235 236

        osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target);
237
        float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target);
238

239
        storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS);
240

241
        if (storage.mReadyToAttack)
242
        {
scrawl's avatar
scrawl committed
243
            storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
244 245 246 247
            // start new attack
            storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);

            if (isRangedCombat)
248
            {
249
                // rotate actor taking into account target movement direction and projectile speed
scrawl's avatar
scrawl committed
250
                osg::Vec3f& lastTargetPos = storage.mLastTargetPos;
251
                vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength);
terrorfisch's avatar
terrorfisch committed
252
                lastTargetPos = vTargetPos;
253

254 255
                storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
                storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir);
256
            }
257
            else
258
            {
259 260
                storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
                storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated
dteviot's avatar
dteviot committed
261
            }
mrcheko's avatar
mrcheko committed
262
        }
263
        return false;
mrcheko's avatar
mrcheko committed
264
    }
gus's avatar
gus committed
265

266
    void MWMechanics::AiCombat::updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage)
267 268
    {
        static const float LOS_UPDATE_DURATION = 0.5f;
269
        if (storage.mUpdateLOSTimer <= 0.f)
270
        {
271 272
            storage.mLOS = MWBase::Environment::get().getWorld()->getLOS(actor, target);
            storage.mUpdateLOSTimer = LOS_UPDATE_DURATION;
273 274
        }
        else
275 276 277 278 279 280 281 282
            storage.mUpdateLOSTimer -= duration;
    }

    void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage)
    {
        static const float BLIND_RUN_DURATION = 1.0f;

        updateLOS(actor, target, duration, storage);
283 284 285 286 287 288 289 290 291 292 293

        AiCombatStorage::FleeState& state = storage.mFleeState;
        switch (state)
        {
            case AiCombatStorage::FleeState_None:
                return;

            case AiCombatStorage::FleeState_Idle:
                {
                    float triggerDist = getMaxAttackDistance(target);

294
                    if (storage.mLOS &&
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
                            (triggerDist >= 1000 || getDistanceMinusHalfExtents(actor, target) <= triggerDist))
                    {
                        const ESM::Pathgrid* pathgrid =
                                MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*storage.mCell->getCell());

                        bool runFallback = true;

                        if (pathgrid && !actor.getClass().isPureWaterCreature(actor))
                        {
                            ESM::Pathgrid::PointList points;
                            CoordinateConverter coords(storage.mCell->getCell());

                            osg::Vec3f localPos = actor.getRefData().getPosition().asVec3();
                            coords.toLocal(localPos);

elsid's avatar
elsid committed
310
                            int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos);
311 312
                            for (int i = 0; i < static_cast<int>(pathgrid->mPoints.size()); i++)
                            {
313
                                if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i))
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
                                {
                                    points.push_back(pathgrid->mPoints[static_cast<size_t>(i)]);
                                }
                            }

                            if (!points.empty())
                            {
                                ESM::Pathgrid::Point dest = points[Misc::Rng::rollDice(points.size())];
                                coords.toWorld(dest);

                                state = AiCombatStorage::FleeState_RunToDestination;
                                storage.mFleeDest = ESM::Pathgrid::Point(dest.mX, dest.mY, dest.mZ);

                                runFallback = false;
                            }
                        }

                        if (runFallback)
                        {
                            state = AiCombatStorage::FleeState_RunBlindly;
                            storage.mFleeBlindRunTimer = 0.0f;
                        }
                    }
                }
                break;

            case AiCombatStorage::FleeState_RunBlindly:
                {
                    // timer to prevent twitchy movement that can be observed in vanilla MW
                    if (storage.mFleeBlindRunTimer < BLIND_RUN_DURATION)
                    {
                        storage.mFleeBlindRunTimer += duration;

                        storage.mMovement.mRotation[2] = osg::PI + getZAngleToDir(target.getRefData().getPosition().asVec3()-actor.getRefData().getPosition().asVec3());
                        storage.mMovement.mPosition[1] = 1;
                        updateActorsMovement(actor, duration, storage);
                    }
                    else
                        state = AiCombatStorage::FleeState_Idle;
                }
                break;

            case AiCombatStorage::FleeState_RunToDestination:
                {
358
                    static const float fFleeDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fFleeDistance")->mValue.getFloat();
359 360

                    float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
361
                    if ((dist > fFleeDistance && !storage.mLOS)
elsid's avatar
elsid committed
362
                            || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration))
363 364 365 366 367 368 369 370
                    {
                        state = AiCombatStorage::FleeState_Idle;
                    }
                }
                break;
        };
    }

371
    void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage)
372
    {
373
        // apply combat movement
374
        MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
375 376 377 378
        actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0];
        actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1];
        actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];

379 380
        rotateActorOnAxis(actor, 2, actorMovementSettings, storage);
        rotateActorOnAxis(actor, 0, actorMovementSettings, storage);
381
    }
382

383
    void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, 
384
        MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage)
385 386
    {
        actorMovementSettings.mRotation[axis] = 0;
387
        float& targetAngleRadians = storage.mMovement.mRotation[axis];
388 389
        if (targetAngleRadians != 0)
        {
390 391 392 393 394 395 396 397 398 399 400 401
            // Some attack animations contain small amount of movement.
            // Since we use cone shapes for melee, we can use a threshold to avoid jittering
            std::shared_ptr<Action>& currentAction = storage.mCurrentAction;
            bool isRangedCombat = false;
            currentAction->getCombatRange(isRangedCombat);
            // Check if the actor now facing desired direction, no need to turn any more
            if (isRangedCombat)
            {
                if (smoothTurn(actor, targetAngleRadians, axis))
                    targetAngleRadians = 0;
            }
            else
402
            {
403 404
                if (smoothTurn(actor, targetAngleRadians, axis, osg::DegreesToRadians(3.f)))
                    targetAngleRadians = 0;
405 406
            }
        }
mrcheko's avatar
mrcheko committed
407
    }
gus's avatar
gus committed
408

gus's avatar
gus committed
409 410
    int AiCombat::getTypeId() const
    {
411
        return TypeIdCombat;
412 413 414 415 416
    }

    unsigned int AiCombat::getPriority() const
    {
        return 1;
gus's avatar
gus committed
417 418
    }

419
    MWWorld::Ptr AiCombat::getTarget() const
420
    {
421
        return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId);
422 423
    }

gus's avatar
gus committed
424 425 426 427
    AiCombat *MWMechanics::AiCombat::clone() const
    {
        return new AiCombat(*this);
    }
scrawl's avatar
scrawl committed
428 429 430

    void AiCombat::writeState(ESM::AiSequence::AiSequence &sequence) const
    {
431
        std::unique_ptr<ESM::AiSequence::AiCombat> combat(new ESM::AiSequence::AiCombat());
scrawl's avatar
scrawl committed
432 433 434 435 436 437 438
        combat->mTargetActorId = mTargetActorId;

        ESM::AiSequence::AiPackageContainer package;
        package.mType = ESM::AiSequence::Ai_Combat;
        package.mPackage = combat.release();
        sequence.mPackages.push_back(package);
    }
439

scrawl's avatar
scrawl committed
440
    void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target)
441
    {
442 443 444 445 446 447 448 449 450 451 452 453 454
        // get the range of the target's weapon
        MWWorld::Ptr targetWeapon = MWWorld::Ptr();
        const MWWorld::Class& targetClass = target.getClass();

        if (targetClass.hasInventoryStore(target))
        {
            MWMechanics::WeaponType weapType = WeapType_None;
            MWWorld::ContainerStoreIterator weaponSlot =
                MWMechanics::getActiveWeapon(targetClass.getCreatureStats(target), targetClass.getInventoryStore(target), &weapType);
            if (weapType != WeapType_PickProbe && weapType != WeapType_Spell && weapType != WeapType_None && weapType != WeapType_HandToHand)
                targetWeapon = *weaponSlot;
        }

455 456
        bool targetUsesRanged = false;
        float rangeAttackOfTarget = ActionWeapon(targetWeapon).getCombatRange(targetUsesRanged);
457
        
458 459 460 461 462
        if (mMovement.mPosition[0] || mMovement.mPosition[1])
        {
            mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
            mCombatMove = true;
        }
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
        else if (isDistantCombat)
        {
            // Backing up behaviour
            // Actor backs up slightly further away than opponent's weapon range
            // (in vanilla - only as far as oponent's weapon range),
            // or not at all if opponent is using a ranged weapon

            if (targetUsesRanged || distToTarget > rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon
                return;

            // actor should not back up into water
            if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f))
                return;

            int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door;

            // Actor can not back up if there is no free space behind
            // Currently we take the 35% of actor's height from the ground as vector height.
            // This approach allows us to detect small obstacles (e.g. crates) and curved walls.
            osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor);
            osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
            osg::Vec3f source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z());
            osg::Vec3f fallbackDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,-1,0);
            osg::Vec3f destination = source + fallbackDirection * (halfExtents.y() + 16);

            bool isObstacleDetected = MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask);
            if (isObstacleDetected)
                return;

            // Check if there is nothing behind - probably actor is near cliff.
            // A current approach: cast ray 1.5-yard ray down in 1.5 yard behind actor from 35% of actor's height.
            // If we did not hit anything, there is a cliff behind actor.
            source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()) + fallbackDirection * (halfExtents.y() + 96);
            destination = source - osg::Vec3f(0, 0, 0.75f * halfExtents.z() + 96);
            bool isCliffDetected = !MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask);
            if (isCliffDetected)
                return;

            mMovement.mPosition[1] = -1;
        }
503
        // dodge movements (for NPCs and bipedal creatures)
504
        // Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff
Allofich's avatar
Allofich committed
505
        else if (actor.getClass().isBipedal(actor))
506
        {
507
            // apply sideway movement (kind of dodging) with some probability
508
            // if actor is within range of target's weapon
509
            if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
510
            {
511
                mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right
512
                mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
                mCombatMove = true;
            }
        }
    }

    void AiCombatStorage::updateCombatMove(float duration)
    {
        if (mCombatMove)
        {
            mTimerCombatMove -= duration;
            if (mTimerCombatMove <= 0)
            {
                stopCombatMove();
            }
        }
    }

    void AiCombatStorage::stopCombatMove()
    {
        mTimerCombatMove = 0;
        mMovement.mPosition[1] = mMovement.mPosition[0] = 0;
        mCombatMove = false;
    }
536 537 538 539 540 541 542 543 544 545 546 547

    void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, 
        const ESM::Weapon* weapon, bool distantCombat)
    {
        if (mReadyToAttack && characterController.readyToStartAttack())
        {
            if (mAttackCooldown <= 0)
            {
                mAttack = true; // attack starts just now
                characterController.setAttackingOrSpell(true);

                if (!distantCombat)
548
                    characterController.setAIAttackType(chooseBestAttack(weapon));
549 550 551 552 553

                mStrength = Misc::Rng::rollClosedProbability();

                const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();

554
                float baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayCreature")->mValue.getFloat();
555 556
                if (actor.getClass().isNpc())
                {
557
                    baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->mValue.getFloat();
558 559

                    //say a provoking combat phrase
560
                    int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->mValue.getInteger();
561 562 563 564 565 566 567 568
                    if (Misc::Rng::roll0to99() < chance)
                    {
                        MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
                    }
                }
                mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9);
            }
            else
569
                mAttackCooldown -= AI_REACTION_TIME;
570 571
        }
    }
572 573 574 575 576 577 578 579 580

    void AiCombatStorage::updateAttack(CharacterController& characterController)
    {
        if (mAttack && (characterController.getAttackStrength() >= mStrength || characterController.readyToPrepareAttack()))
        {
            mAttack = false;
        }
        characterController.setAttackingOrSpell(mAttack);
    }
581 582 583 584 585 586 587 588 589

    void AiCombatStorage::stopAttack()
    {
        mMovement.mPosition[0] = 0;
        mMovement.mPosition[1] = 0;
        mMovement.mPosition[2] = 0;
        mReadyToAttack = false;
        mAttack = false;
    }
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609

    void AiCombatStorage::startFleeing()
    {
        stopFleeing();
        mFleeState = FleeState_Idle;
    }

    void AiCombatStorage::stopFleeing()
    {
        mMovement.mPosition[0] = 0;
        mMovement.mPosition[1] = 0;
        mMovement.mPosition[2] = 0;
        mFleeState = FleeState_None;
        mFleeDest = ESM::Pathgrid::Point(0, 0, 0);
    }

    bool AiCombatStorage::isFleeing()
    {
        return mFleeState != FleeState_None;
    }
mrcheko's avatar
mrcheko committed
610
}
mrcheko's avatar
mrcheko committed
611

mrcheko's avatar
mrcheko committed
612

mrcheko's avatar
mrcheko committed
613 614
namespace
{
mrcheko's avatar
mrcheko committed
615

616
std::string chooseBestAttack(const ESM::Weapon* weapon)
mrcheko's avatar
mrcheko committed
617
{
618
    std::string attackType;
619

620
    if (weapon != nullptr)
621 622 623 624 625 626
    {
        //the more damage attackType deals the more probability it has
        int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2;
        int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2;
        int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2;

627 628
        float roll = Misc::Rng::rollClosedProbability() * (slash + chop + thrust);
        if(roll <= slash)
629
            attackType = "slash";
630
        else if(roll <= (slash + thrust))
631
            attackType = "thrust";
632
        else
633
            attackType = "chop";
634
    }
635 636
    else
        MWMechanics::CharacterController::setAttackTypeRandomly(attackType);
637 638 639 640

    return attackType;
}

scrawl's avatar
scrawl committed
641
osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos,
642 643 644
    float duration, int weapType, float strength)
{
    float projSpeed;
645
    const MWWorld::Store<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
646

647 648
    // get projectile speed (depending on weapon type)
    if (weapType == ESM::Weapon::MarksmanThrown)
mrcheko's avatar
mrcheko committed
649
    {
650 651
        static float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->mValue.getFloat();
        static float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->mValue.getFloat();
652

653
        projSpeed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength;
mrcheko's avatar
mrcheko committed
654
    }
655
    else if (weapType != 0)
656
    {
657 658
        static float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->mValue.getFloat();
        static float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat();
659

660 661 662 663
        projSpeed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength;
    }
    else // weapType is 0 ==> it's a target spell projectile
    {
664
        projSpeed = gmst.find("fTargetSpellMaxSpeed")->mValue.getFloat();
665 666 667 668
    }

    // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same

scrawl's avatar
scrawl committed
669
    osg::Vec3f vTargetPos = target.getRefData().getPosition().asVec3();
670
    osg::Vec3f vDirToTarget = MWBase::Environment::get().getWorld()->aimToTarget(actor, target);
671 672
    float distToTarget = vDirToTarget.length();

scrawl's avatar
scrawl committed
673
    osg::Vec3f vTargetMoveDir = vTargetPos - vLastTargetPos;
674 675
    vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now

scrawl's avatar
scrawl committed
676
    osg::Vec3f vPerpToDir = vDirToTarget ^ osg::Vec3f(0,0,1); // cross product
677

scrawl's avatar
scrawl committed
678 679 680 681 682 683 684
    vPerpToDir.normalize();
    osg::Vec3f vDirToTargetNormalized = vDirToTarget;
    vDirToTargetNormalized.normalize();

    // dot product
    float velPerp = vTargetMoveDir * vPerpToDir;
    float velDir = vTargetMoveDir * vDirToTargetNormalized;
685 686 687 688 689

    // time to collision between target and projectile
    float t_collision;

    float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp;
scrawl's avatar
scrawl committed
690 691 692 693 694 695

    osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir;
    vTargetMoveDirNormalized.normalize();

    float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product
    projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff);
696 697

    if (projVelDirSquared > 0)
scrawl's avatar
scrawl committed
698
        t_collision = projDistDiff / (std::sqrt(projVelDirSquared) - velDir);
699 700
    else t_collision = 0; // speed of projectile is not enough to reach moving target

701
    return vDirToTarget + vTargetMoveDir * t_collision;
gus's avatar
gus committed
702 703
}

mrcheko's avatar
mrcheko committed
704
}