예제 #1
0
        private bool MeleeAnimation()
        {
            // Are we in range and facing target? Then start attack.
            if (senses.TargetInSight && senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos))
            {
                float distance = MeleeDistance;
                // Classic uses separate melee distance for targeting player and for targeting other AI
                if (!DaggerfallUnity.Settings.EnhancedCombatAI && senses.Target != GameManager.Instance.PlayerEntityBehaviour)
                {
                    distance = ClassicMeleeDistanceVsAI;
                }

                // Take the rate of target approach into account when deciding if to attack
                if (senses.DistanceToTarget > distance + senses.TargetRateOfApproach)
                {
                    return(false);
                }

                // Set melee animation state
                mobile.ChangeEnemyState(MobileStates.PrimaryAttack);

                // Play melee sound
                if (sounds)
                {
                    sounds.PlayAttackSound();
                }

                return(true);
            }

            return(false);
        }
예제 #2
0
        private 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)
                {
                    controller.Move(Vector3.zero);
                }
                else
                {
                    controller.SimpleMove(Vector3.zero);
                }

                return;
            }
            else
            {
                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.
                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));
                }

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

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

                return;
            }

            // Monster speed of movement follows the same formula as for when the player walks
            EnemyEntity entity    = entityBehaviour.Entity as EnemyEntity;
            float       moveSpeed = ((entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) / PlayerSpeedChanger.classicToUnitySpeedUnitRatio);

            // Reduced speed if playing a one-shot animation
            if (mobile.IsPlayingOneShot())
            {
                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--;
            }

            // Enemy will keep moving towards last known target position
            targetPos = senses.LastKnownTargetPos;

            // Remain idle after finishing any attacks if no target or after giving up finding the target
            if (entityBehaviour.Target == null || giveUpTimer == 0 || targetPos == EnemySenses.ResetPlayerPos)
            {
                if (!mobile.IsPlayingOneShot())
                {
                    mobile.ChangeEnemyState(MobileStates.Idle);
                }

                return;
            }

            // 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;
            }

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

            // If attacking, randomly follow target with attack.
            if (mobile.Summary.EnemyState == MobileStates.PrimaryAttack)
            {
                if (!isAttackFollowsPlayerSet)
                {
                    attackFollowsPlayer      = (Random.Range(0f, 1f) > 0.5f);
                    isAttackFollowsPlayerSet = true;
                }
            }
            else
            {
                isAttackFollowsPlayerSet = false;
            }

            if (attackFollowsPlayer)
            {
                transform.forward = direction.normalized;
            }

            // Bow attack for enemies that have the appropriate animation
            if (senses.TargetInSight && 360 * MeshReader.GlobalScale < distance && distance < 2048 * MeshReader.GlobalScale)
            {
                if (senses.TargetIsWithinYawAngle(22.5f))
                {
                    if (mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132)
                    {
                        // Random chance to shoot bow
                        if (classicUpdate && DFRandom.rand() < 1000)
                        {
                            if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2 &&
                                mobile.Summary.EnemyState != MobileStates.RangedAttack1)
                            {
                                mobile.ChangeEnemyState(MobileStates.RangedAttack1);
                            }
                            else if (mobile.Summary.Enemy.HasRangedAttack2 && mobile.Summary.EnemyState != MobileStates.RangedAttack2)
                            {
                                mobile.ChangeEnemyState(MobileStates.RangedAttack2);
                            }
                        }
                        // Otherwise hold ground
                        else if (!mobile.IsPlayingOneShot())
                        {
                            mobile.ChangeEnemyState(MobileStates.Idle);
                        }
                    }
                    //else if (spellPoints > 0 && canCastRangeSpells && DFRandom.rand() % 40 == 0) TODO: Ranged spell shooting
                    //          CastRangedSpell();
                    //          Spell Cast Animation;
                    else
                    {
                        // If no ranged attack, move towards target
                        PursueTarget(direction, moveSpeed);
                    }
                }
                else
                {
                    if (!mobile.IsPlayingOneShot())
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }
                    TurnToTarget(direction.normalized);
                    return;
                }
            }
            // Move towards target
            else if (distance > stopDistance)
            {
                PursueTarget(direction, moveSpeed);
            }
            else if (!senses.TargetIsWithinYawAngle(22.5f))
            {
                TurnToTarget(direction.normalized);
            }
            //else
            //{
            // TODO: Touch spells.
            //if (hasSpellPoints && attackCoolDownFinished && CanCastTouchSpells)
            //{
            //    Cast Touch Spell
            //    Spell Cast Animation
            //}
            //}
            else if (!senses.DetectedTarget && mobile.Summary.EnemyState == MobileStates.Move)
            {
                mobile.ChangeEnemyState(MobileStates.Idle);
            }
        }
예제 #3
0
        private void Move()
        {
            // 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.
                if (knockBackSpeed > (40 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)))
                {
                    knockBackSpeed = (40 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10));
                }

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

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

                if (swims)
                {
                    WaterMove(motion);
                }
                else if (flies)
                {
                    controller.Move(motion * Time.deltaTime);
                }
                else
                {
                    controller.SimpleMove(motion);
                }

                if (classicUpdate)
                {
                    knockBackSpeed -= (5 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10));
                    if (knockBackSpeed <= (5 / (PlayerMotor.classicToUnitySpeedUnitRatio / 10)) &&
                        mobile.Summary.EnemyState != MobileStates.PrimaryAttack)
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }
                }

                return;
            }

            // Monster speed of movement follows the same formula as for when the player walks
            EnemyEntity entity    = entityBehaviour.Entity as EnemyEntity;
            float       moveSpeed = ((entity.Stats.LiveSpeed + PlayerMotor.dfWalkBase) / PlayerMotor.classicToUnitySpeedUnitRatio);

            // Reduced speed if playing a one-shot animation
            if (mobile.IsPlayingOneShot())
            {
                moveSpeed /= AttackSpeedDivisor;
            }

            // Remain idle when not hostile
            if (!isHostile)
            {
                mobile.ChangeEnemyState(MobileStates.Idle);
                return;
            }
            // If hostile but the enemy doesn't see the player, run the stealth check
            else if (senses.LastKnownPlayerPos == EnemySenses.ResetPlayerPos)
            {
                if (senses.StealthCheck())
                {
                    // Enemy noticed the player
                    senses.LastKnownPlayerPos = GameManager.Instance.PlayerObject.transform.position;
                    senses.DetectedPlayer     = true;
                }
                else
                {
                    // Enemy didn't notice the player
                    mobile.ChangeEnemyState(MobileStates.Idle);
                    senses.DetectedPlayer = false;
                    return;
                }
            }

            // As long as the player is directly seen/heard,
            // giveUpTimer is reset to full
            if (senses.PlayerInSight || senses.PlayerInEarshot)
            {
                giveUpTimer = 200;
            }
            else if (giveUpTimer == 0)
            {
                // Player lost for too long, or wasn't in sight/earshot to begin with. Time to give up
                senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                return;
            }

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

            // Enemy will keep moving towards last known player position
            targetPos = senses.LastKnownPlayerPos;

            // Flying enemies and slaughterfish aim for player face
            if (flies || (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 player
                // Otherwise, their target vector aims up towards the player
                var playerController = GameManager.Instance.PlayerController;
                var deltaHeight      = (playerController.height - controller.height) / 2;
                targetPos.y -= deltaHeight;
            }

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

            // If attacking, randomly follow player with attack.
            if (mobile.Summary.EnemyState == MobileStates.PrimaryAttack)
            {
                if (!isAttackFollowsPlayerSet)
                {
                    attackFollowsPlayer      = (Random.Range(0f, 1f) > 0.5f);
                    isAttackFollowsPlayerSet = true;
                }
            }
            else
            {
                isAttackFollowsPlayerSet = false;
            }

            if (attackFollowsPlayer)
            {
                transform.forward = direction.normalized;
            }

            // Bow attack for enemies that have the appropriate animation
            if (senses.PlayerInSight && 360 * MeshReader.GlobalScale < distance && distance < 2048 * MeshReader.GlobalScale)
            {
                if (senses.TargetIsWithinYawAngle(22.5f))
                {
                    if (mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132)
                    {
                        // Random chance to shoot bow
                        if (DFRandom.rand() < 1000)
                        {
                            if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2 &&
                                mobile.Summary.EnemyState != MobileStates.RangedAttack1)
                            {
                                mobile.ChangeEnemyState(MobileStates.RangedAttack1);
                            }
                            else if (mobile.Summary.Enemy.HasRangedAttack2 && mobile.Summary.EnemyState != MobileStates.RangedAttack2)
                            {
                                mobile.ChangeEnemyState(MobileStates.RangedAttack2);
                            }
                        }
                        // Otherwise hold ground
                        else if (!mobile.IsPlayingOneShot())
                        {
                            mobile.ChangeEnemyState(MobileStates.Idle);
                        }
                    }
                    //else if (spellPoints > 0 && canCastRangeSpells && DFRandom.rand() % 40 == 0) TODO: Ranged spell shooting
                    //          CastRangedSpell();
                    //          Spell Cast Animation;
                    else
                    {
                        // If no ranged attack, move towards target
                        PursueTarget(direction, moveSpeed);
                    }
                }
                else
                {
                    if (!mobile.IsPlayingOneShot())
                    {
                        mobile.ChangeEnemyState(MobileStates.Move);
                    }
                    TurnToTarget(direction.normalized);
                    return;
                }
            }
            // Move towards target
            else if (distance > stopDistance)
            {
                PursueTarget(direction, moveSpeed);
            }
            else if (!senses.TargetIsWithinYawAngle(22.5f))
            {
                TurnToTarget(direction.normalized);
            }
            //else
            //{
            // TODO: Touch spells.
            //if (hasSpellPoints && attackCoolDownFinished && CanCastTouchSpells)
            //{
            //    Cast Touch Spell
            //    Spell Cast Animation
            //}
            //}
            else if (!senses.PlayerInSight && !senses.PlayerInEarshot)
            {
                mobile.ChangeEnemyState(MobileStates.Idle);
            }
        }
예제 #4
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;
            }
        }
예제 #5
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;
            }
        }