Exemple #1
0
        /// <summary>
        /// Make decision about what movement action to take.
        /// </summary>
        void Move()
        {
            // Cancel movement and animations if paralyzed, but still allow gravity to take effect
            // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air
            // Paralyzed swimming enemies will just freeze in place
            // Freezing anims also prevents the attack from triggering until paralysis cleared
            if (entityBehaviour.Entity.IsParalyzed)
            {
                mobile.FreezeAnims = true;

                if ((swims || flies) && !isLevitating)
                {
                    controller.Move(Vector3.zero);
                }
                else
                {
                    controller.SimpleMove(Vector3.zero);
                }

                return;
            }
            mobile.FreezeAnims = false;

            // Apply gravity to non-moving AI if active (has a combat target) or nearby
            if ((entityBehaviour.Target != null || senses.WouldBeSpawnedInClassic) && !flies && !swims)
            {
                controller.SimpleMove(Vector3.zero);
            }

            // If hit, get knocked back
            if (knockBackSpeed > 0)
            {
                // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion,
                // making it last longer and do more damage if the enemy collides with something (TODO).
                if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)))
                {
                    knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10));
                }

                if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) &&
                    mobile.Summary.EnemyState != MobileStates.PrimaryAttack)
                {
                    mobile.ChangeEnemyState(MobileStates.Hurt);
                }

                // Actual speed of motion is limited
                Vector3 motion;
                if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)))
                {
                    motion = knockBackDirection * knockBackSpeed;
                }
                else
                {
                    motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10));
                }

                // Move in direction of knockback
                if (swims)
                {
                    WaterMove(motion);
                }
                else if (flies || isLevitating)
                {
                    controller.Move(motion * Time.deltaTime);
                }
                else
                {
                    controller.SimpleMove(motion);
                }

                // Remove remaining knockback and restore animation
                if (classicUpdate)
                {
                    knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10));
                    if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) &&
                        mobile.Summary.EnemyState != MobileStates.PrimaryAttack)
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }
                }

                // If a decent hit got in, reconsider whether to continue current tactic
                if (knockBackSpeed > (10 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)))
                {
                    EvaluateMoveInForAttack();
                }

                return;
            }

            // Monster speed of movement follows the same formula as for when the player walks
            float moveSpeed = (entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale;

            // Reduced speed if playing a one-shot animation with enhanced AI
            if (mobile.IsPlayingOneShot() && DaggerfallUnity.Settings.EnhancedCombatAI)
            {
                moveSpeed /= AttackSpeedDivisor;
            }

            // As long as the target is detected,
            // giveUpTimer is reset to full
            if (senses.DetectedTarget)
            {
                giveUpTimer = 200;
            }

            // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop
            if (!senses.DetectedTarget &&
                giveUpTimer > 0 && classicUpdate)
            {
                giveUpTimer--;
            }

            // Change to idle animation if haven't moved or rotated
            if (!mobile.IsPlayingOneShot())
            {
                // Rotation is done at classic update rate, so check at classic update rate
                if (classicUpdate)
                {
                    Vector3 currentDirection = transform.forward;
                    currentDirection.y = 0;

                    if (lastPosition == transform.position && lastDirection == currentDirection)
                    {
                        mobile.ChangeEnemyState(MobileStates.Idle);
                        rotating = false;
                    }
                    else
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }

                    lastDirection = currentDirection;
                }
                // Movement is done at regular update rate, so check at regular update rate
                else if (!rotating && lastPosition == transform.position)
                {
                    mobile.ChangeEnemyState(MobileStates.Idle);
                }
                else
                {
                    mobile.ChangeEnemyState(MobileStates.Move);
                }

                lastPosition = transform.position;
            }

            // Do nothing if no target or after giving up finding the target
            if (entityBehaviour.Target == null || giveUpTimer == 0)
            {
                SetChangeStateTimer();

                return;
            }

            // Get predicted target position
            if (avoidObstaclesTimer == 0 && !lookingForDetour)
            {
                targetPos = senses.PredictedTargetPos;
                // Flying enemies and slaughterfish aim for target face
                if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish))
                {
                    targetPos.y += 0.9f;
                }
                else
                {
                    // Ground enemies target at their own height
                    // This avoids short enemies from stepping on each other as they approach the target
                    // Otherwise, their target vector aims up towards the target
                    var playerController = GameManager.Instance.PlayerController;
                    var deltaHeight      = (playerController.height - controller.height) / 2;
                    targetPos.y -= deltaHeight;
                }
                tempMovePos = targetPos;
            }
            else
            {
                targetPos = tempMovePos;
            }

            // Get direction & distance.
            var   direction = targetPos - transform.position;
            float distance  = (targetPos - transform.position).magnitude;

            // Ranged attacks
            if (senses.TargetInSight && 360 * MeshReader.GlobalScale < senses.DistanceToTarget && senses.DistanceToTarget < 2048 * MeshReader.GlobalScale)
            {
                bool evaluateBow         = mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132;
                bool evaluateRangedMagic = false;
                if (!evaluateBow)
                {
                    evaluateRangedMagic = CanCastRangedSpell();
                }

                if (evaluateBow || evaluateRangedMagic)
                {
                    if (senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos))
                    {
                        if (!mobile.IsPlayingOneShot())
                        {
                            if (evaluateBow)
                            {
                                // Random chance to shoot bow
                                if (classicUpdate && DFRandom.rand() < 1000)
                                {
                                    if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2)
                                    {
                                        mobile.ChangeEnemyState(MobileStates.RangedAttack1);
                                    }
                                    else if (mobile.Summary.Enemy.HasRangedAttack2)
                                    {
                                        mobile.ChangeEnemyState(MobileStates.RangedAttack2);
                                    }
                                }
                            }
                            // Random chance to shoot spell
                            else if (classicUpdate && DFRandom.rand() % 40 == 0 &&
                                     entityEffectManager.SetReadySpell(selectedSpell))
                            {
                                mobile.ChangeEnemyState(MobileStates.Spell);
                            }
                        }
                    }
                    else
                    {
                        TurnToTarget(direction.normalized);
                    }

                    return;
                }
            }

            if (senses.TargetInSight && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance +
                senses.TargetRateOfApproach && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell))
            {
                if (mobile.Summary.EnemyState != MobileStates.Spell)
                {
                    mobile.ChangeEnemyState(MobileStates.Spell);
                }

                attack.ResetMeleeTimer();
                return;
            }

            // Update melee decision
            if (moveInForAttackTimer <= 0 && avoidObstaclesTimer == 0 && !lookingForDetour)
            {
                EvaluateMoveInForAttack();
            }
            if (moveInForAttackTimer > 0)
            {
                moveInForAttackTimer -= Time.deltaTime;
            }

            if (avoidObstaclesTimer > 0)
            {
                avoidObstaclesTimer -= Time.deltaTime;
            }
            if (avoidObstaclesTimer < 0)
            {
                avoidObstaclesTimer = 0;
            }

            if (changeStateTimer > 0)
            {
                changeStateTimer -= Time.deltaTime;
            }

            // Looking for detour
            if (lookingForDetour)
            {
                CombatMove(direction, moveSpeed);
            }
            // Approach target until we are close enough to be on-guard, or continue to melee range if attacking
            else if ((!retreating && distance >= (stopDistance * 2.75)) ||
                     (distance > stopDistance && moveInForAttack))
            {
                // If state change timer is done, or we are already pursuing, we can move
                if (changeStateTimer <= 0 || pursuing)
                {
                    CombatMove(direction, moveSpeed);
                }
                // Otherwise, just keep an eye on target until timer finishes
                else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos))
                {
                    TurnToTarget(direction.normalized);
                }
            }
            // Back away if right next to target, if retreating, or if cooling down from attack
            // Classic AI never backs away
            else if (DaggerfallUnity.Settings.EnhancedCombatAI && (senses.TargetInSight && (distance < stopDistance * .50 ||
                                                                                            (!moveInForAttack && distance < (stopDistance * retreatDistanceMultiplier)))))
            {
                // If state change timer is done, or we are already retreating, we can move
                if (changeStateTimer <= 0 || retreating)
                {
                    CombatMove(direction, moveSpeed / 2, true);
                }
                // Otherwise, just keep an eye on target until timer finishes
                else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos))
                {
                    TurnToTarget(direction.normalized);
                }
            }
            else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos))
            {
                TurnToTarget(direction.normalized);
            }
            else if (avoidObstaclesTimer > 0 && distance > 0.1f)
            {
                CombatMove(direction, moveSpeed);
            }
            else // Next to target
            {
                SetChangeStateTimer();
                pursuing   = false;
                retreating = false;

                avoidObstaclesTimer = 0;
            }
        }
        /// <summary>
        /// Make decision about what movement action to take.
        /// </summary>
        void Move()
        {
            // Cancel movement and animations if paralyzed, but still allow gravity to take effect
            // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air
            // Paralyzed swimming enemies will just freeze in place
            // Freezing anims also prevents the attack from triggering until paralysis cleared
            if (entityBehaviour.Entity.IsParalyzed)
            {
                mobile.FreezeAnims = true;

                if ((swims || flies) && !isLevitating)
                {
                    controller.Move(Vector3.zero);
                }
                else
                {
                    controller.SimpleMove(Vector3.zero);
                }

                return;
            }
            mobile.FreezeAnims = false;

            // If hit, get knocked back
            if (knockBackSpeed > 0)
            {
                // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion,
                // making it last longer and do more damage if the enemy collides with something (TODO).
                if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)))
                {
                    knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10));
                }

                if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) &&
                    mobile.Summary.EnemyState != MobileStates.PrimaryAttack)
                {
                    mobile.ChangeEnemyState(MobileStates.Hurt);
                }

                // Actual speed of motion is limited
                Vector3 motion;
                if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)))
                {
                    motion = knockBackDirection * knockBackSpeed;
                }
                else
                {
                    motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10));
                }

                // Move in direction of knockback
                if (swims)
                {
                    WaterMove(motion);
                }
                else if (flies || isLevitating)
                {
                    controller.Move(motion * Time.deltaTime);
                }
                else
                {
                    controller.SimpleMove(motion);
                }

                // Remove remaining knockback and restore animation
                if (classicUpdate)
                {
                    knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10));
                    if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) &&
                        mobile.Summary.EnemyState != MobileStates.PrimaryAttack)
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }
                }

                // If a decent hit got in, reconsider whether to continue current tactic
                if (knockBackSpeed > (10 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)))
                {
                    EvaluateMoveInForAttack();
                }

                return;
            }

            // Apply gravity
            if (!flies && !swims && !isLevitating && !controller.isGrounded)
            {
                controller.SimpleMove(Vector3.zero);

                // Only return if actually falling. Sometimes mobiles can get stuck where they are !isGrounded but SimpleMove(Vector3.zero) doesn't help.
                // Allowing them to continue and attempt a Move() in the code below frees them, but we don't want to allow that if we can avoid it so they aren't moving
                // while falling, which can also accelerate the fall due to anti-bounce downward movement in Move().
                if (lastPosition != transform.position)
                {
                    return;
                }
            }

            // Monster speed of movement follows the same formula as for when the player walks
            float moveSpeed = (entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale;

            // Get isPlayingOneShot for use below
            bool isPlayingOneShot = mobile.IsPlayingOneShot();

            // Reduced speed if playing a one-shot animation with enhanced AI
            if (isPlayingOneShot && DaggerfallUnity.Settings.EnhancedCombatAI)
            {
                moveSpeed /= AttackSpeedDivisor;
            }

            // As long as the target is detected,
            // giveUpTimer is reset to full
            if (senses.DetectedTarget)
            {
                giveUpTimer = 200;
            }

            // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop
            if (classicUpdate && !senses.DetectedTarget && giveUpTimer > 0)
            {
                giveUpTimer--;
            }

            // Change to idle animation if haven't moved or rotated
            if (!mobile.IsPlayingOneShot())
            {
                // Rotation is done at classic update rate, so check at classic update rate
                if (classicUpdate)
                {
                    Vector3 currentDirection = transform.forward;
                    currentDirection.y = 0;

                    if (lastPosition == transform.position && lastDirection == currentDirection)
                    {
                        mobile.ChangeEnemyState(MobileStates.Idle);
                        rotating = false;
                    }
                    else
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }

                    lastDirection = currentDirection;
                }
                // Movement is done at regular update rate, so check at regular update rate
                else if (!rotating && lastPosition == transform.position)
                {
                    mobile.ChangeEnemyState(MobileStates.Idle);
                }
                else
                {
                    mobile.ChangeEnemyState(MobileStates.Move);
                }

                lastPosition = transform.position;
            }

            // Do nothing if no target or after giving up finding the target or if target position hasn't been acquired yet
            if (senses.Target == null || giveUpTimer == 0 || senses.PredictedTargetPos == EnemySenses.ResetPlayerPos)
            {
                SetChangeStateTimer();
                bashing = false;

                return;
            }

            if (bashing)
            {
                if (senses.TargetInSight || senses.LastKnownDoor == null || !senses.LastKnownDoor.IsLocked)
                {
                    bashing = false;
                }
                else
                {
                    int speed = entity.Stats.LiveSpeed;
                    if (classicUpdate && DFRandom.rand() % speed >= (speed >> 3) + 6 && attack.MeleeTimer == 0)
                    {
                        mobile.ChangeEnemyState(MobileStates.PrimaryAttack);
                        attack.ResetMeleeTimer();
                    }

                    return;
                }
            }

            bool targetPosIsEnemyPos = false;

            // Get location to move towards. Either the combat target's position or, if trying to avoid an obstacle or fall,
            // a location to try to detour around the obstacle/fall.
            if (avoidObstaclesTimer == 0 && (senses.PredictedTargetPos.y > transform.position.y || ClearPathToPosition(senses.PredictedTargetPos)))
            {
                targetPos = senses.PredictedTargetPos;
                // Flying enemies and slaughterfish aim for target face
                if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish))
                {
                    targetPos.y += 0.9f;
                }
                else
                {
                    // Ground enemies target at their own height
                    // This avoids short enemies from stepping on each other as they approach the target
                    // Otherwise, their target vector aims up towards the target
                    var targetController = senses.Target.GetComponent <CharacterController>();
                    var deltaHeight      = (targetController.height - controller.height) / 2;
                    targetPos.y -= deltaHeight;
                }
                tempMovePos         = targetPos;
                targetPosIsEnemyPos = true;
            }
            // If detouring, use the detour position
            else if (avoidObstaclesTimer > 0)
            {
                targetPos = tempMovePos;
            }
            // Otherwise, go straight
            else
            {
                tempMovePos = transform.position + transform.forward * 2;
                targetPos   = tempMovePos;
            }

            // Get direction & distance.
            var   direction = (targetPos - transform.position).normalized;
            float distance  = (targetPos - transform.position).magnitude;

            // Ranged attacks
            if (targetPosIsEnemyPos && senses.DetectedTarget && 360 * MeshReader.GlobalScale < senses.DistanceToTarget && senses.DistanceToTarget < 2048 * MeshReader.GlobalScale)
            {
                bool evaluateBow         = mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132;
                bool evaluateRangedMagic = false;
                if (!evaluateBow)
                {
                    evaluateRangedMagic = CanCastRangedSpell();
                }

                if (evaluateBow || evaluateRangedMagic)
                {
                    if (classicUpdate && senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos))
                    {
                        if (!isPlayingOneShot)
                        {
                            if (evaluateBow)
                            {
                                // Random chance to shoot bow
                                if (DFRandom.rand() < 1000)
                                {
                                    if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2)
                                    {
                                        mobile.ChangeEnemyState(MobileStates.RangedAttack1);
                                    }
                                    else if (mobile.Summary.Enemy.HasRangedAttack2)
                                    {
                                        mobile.ChangeEnemyState(MobileStates.RangedAttack2);
                                    }
                                }
                            }
                            // Random chance to shoot spell
                            else if (DFRandom.rand() % 40 == 0 &&
                                     entityEffectManager.SetReadySpell(selectedSpell))
                            {
                                mobile.ChangeEnemyState(MobileStates.Spell);
                            }
                        }
                    }
                    else
                    {
                        TurnToTarget(direction);
                    }

                    return;
                }
            }

            // Touch spells
            if (targetPosIsEnemyPos && senses.TargetInSight && senses.DetectedTarget && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance +
                senses.TargetRateOfApproach && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell))
            {
                if (mobile.Summary.EnemyState != MobileStates.Spell)
                {
                    mobile.ChangeEnemyState(MobileStates.Spell);
                }

                attack.ResetMeleeTimer();
                return;
            }

            // Update advance/retreat decision
            if (moveInForAttackTimer <= 0 && avoidObstaclesTimer == 0)
            {
                EvaluateMoveInForAttack();
            }

            // Update timers
            if (moveInForAttackTimer > 0)
            {
                moveInForAttackTimer -= Time.deltaTime;
            }

            if (avoidObstaclesTimer > 0 && senses.TargetIsWithinYawAngle(5.625f, targetPos))
            {
                avoidObstaclesTimer -= Time.deltaTime;
            }
            if (avoidObstaclesTimer < 0)
            {
                avoidObstaclesTimer = 0;
            }

            if (checkingClockwiseTimer > 0)
            {
                checkingClockwiseTimer -= Time.deltaTime;
            }
            if (checkingClockwiseTimer < 0)
            {
                checkingClockwiseTimer = 0;
            }

            if (changeStateTimer > 0)
            {
                changeStateTimer -= Time.deltaTime;
            }

            // If detouring, attempt to move
            if (avoidObstaclesTimer > 0)
            {
                AttemptMove(direction, moveSpeed);
            }
            // Otherwise, if not still executing a retreat, approach target until close enough to be on-guard.
            // If decided to move in for attack, continue until within melee range. Classic always moves in for attack.
            else if ((!retreating && distance >= (stopDistance * 2.75)) ||
                     (distance > stopDistance && moveInForAttack))
            {
                // If state change timer is done, or we are continuing an already started combatMove, we can move immediately
                if (changeStateTimer <= 0 || pursuing)
                {
                    AttemptMove(direction, moveSpeed);
                }
                // Otherwise, look at target until timer finishes
                else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos))
                {
                    TurnToTarget(direction);
                }
            }
            // Back away from combat target if right next to it, or if decided to retreat and enemy is too close.
            // Classic AI never backs awwy.
            else if (DaggerfallUnity.Settings.EnhancedCombatAI && senses.TargetInSight && (distance < stopDistance * .50 ||
                                                                                           (!moveInForAttack && distance < (stopDistance * retreatDistanceMultiplier))))
            {
                // If state change timer is done, or we are already executing a retreat, we can move immediately
                if (changeStateTimer <= 0 || retreating)
                {
                    AttemptMove(direction, moveSpeed / 2, true);
                }
                // Otherwise, look at target until timer finishes
                else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos))
                {
                    TurnToTarget(direction.normalized);
                }
            }
            // Not moving, just look at target
            else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos))
            {
                TurnToTarget(direction.normalized);
            }
            else // Not moving, and no need to turn
            {
                SetChangeStateTimer();
                pursuing            = false;
                retreating          = false;
                avoidObstaclesTimer = 0;
            }
        }