Ejemplo n.º 1
0
        void Update()
        {
            // Handle state in progress
            if (mobile.IsPlayingOneShot())
            {
                // Are we attacking?
                if (mobile.IsAttacking())
                {
                    isAttacking = true;
                }

                return;
            }

            // If an attack was in progress it is now complete and we can apply damage
            if (isAttacking)
            {
                MeleeDamage();
                isAttacking = false;
            }

            // Countdown to next attack
            meleeTimer -= Time.deltaTime;
            if (meleeTimer < 0)
            {
                MeleeAnimation();
                meleeTimer = MeleeAttackSpeed;
            }
        }
Ejemplo n.º 2
0
        void Update()
        {
            // Handle state in progress
            if (mobile.IsPlayingOneShot() && (mobile.LastFrameAnimated < mobile.Summary.Enemy.HitFrame))
            {
                // Are we attacking?
                if (mobile.IsAttacking())
                {
                    isAttacking = true;
                }

                return;
            }

            // If an attack was in progress it is now complete and we can apply damage
            if (isAttacking && mobile.LastFrameAnimated == mobile.Summary.Enemy.HitFrame)
            {
                MeleeDamage();
                isAttacking = false;
            }

            // Countdown to next attack
            meleeTimer -= Time.deltaTime;
            if (meleeTimer < 0)
            {
                MeleeAnimation();
                meleeTimer = MeleeAttackSpeed;
                // Randomize
                meleeTimer += Random.Range(-.50f, .50f);
            }
        }
        void FixedUpdate()
        {
            // Unable to attack if AI disabled or paralyzed
            if (GameManager.Instance.DisableAI || entityBehaviour.Entity.IsParalyzed)
            {
                return;
            }

            // Unable to attack when playing certain oneshot anims
            if (mobile && mobile.IsPlayingOneShot() && mobile.OneShotPauseActionsWhilePlaying())
            {
                return;
            }

            // Countdown to next melee attack
            MeleeTimer -= Time.deltaTime;

            if (MeleeTimer < 0)
            {
                MeleeTimer = 0;
            }

            // Note: Speed comparison here is reversed from classic. Classic's way makes fewer attack
            // attempts at higher speeds, so it seems backwards.
            if (GameManager.ClassicUpdate && MeleeTimer == 0)
            {
                if (!MeleeAnimation())
                {
                    return;
                }

                ResetMeleeTimer();
            }
        }
Ejemplo n.º 4
0
        void Update()
        {
            // Handle state in progress before hit frame
            if (mobile.IsPlayingOneShot())
            {
                if (mobile.LastFrameAnimated < mobile.Summary.Enemy.HitFrame &&
                    mobile.Summary.EnemyState == MobileStates.PrimaryAttack)
                {
                    // Are we melee attacking?
                    if (mobile.IsAttacking())
                    {
                        isMeleeAttackingPreHitFrame = true;
                    }

                    return;
                }
                else if (mobile.LastFrameAnimated < 2 && // TODO: Animate bow correctly
                         (mobile.Summary.EnemyState == MobileStates.RangedAttack1 ||
                          mobile.Summary.EnemyState == MobileStates.RangedAttack2))
                {
                    // Are we shooting bow?
                    if (mobile.IsAttacking())
                    {
                        isShootingPreHitFrame = true;
                    }

                    return;
                }
            }

            // If a melee attack has reached the hit frame we can apply damage
            if (isMeleeAttackingPreHitFrame && mobile.LastFrameAnimated == mobile.Summary.Enemy.HitFrame)
            {
                MeleeDamage();
                isMeleeAttackingPreHitFrame = false;
            }
            // Same for shooting bow
            else if (isShootingPreHitFrame && mobile.LastFrameAnimated == 2) // TODO: Animate bow correctly
            {
                BowDamage();
                isShootingPreHitFrame = false;

                DaggerfallAudioSource dfAudioSource = GetComponent <DaggerfallAudioSource>();
                if (dfAudioSource)
                {
                    dfAudioSource.PlayOneShot((int)SoundClips.ArrowShoot, 1, 1.0f);
                }
            }

            // Countdown to next melee attack
            meleeTimer -= Time.deltaTime;
            if (meleeTimer < 0)
            {
                MeleeAnimation();
                meleeTimer = MeleeAttackSpeed;
                // Randomize
                meleeTimer += Random.Range(-.50f, .50f);
            }
        }
Ejemplo n.º 5
0
        void FixedUpdate()
        {
            classicUpdateTimer += Time.deltaTime;
            if (classicUpdateTimer >= PlayerEntity.ClassicUpdateInterval)
            {
                classicUpdateTimer = 0;
                classicUpdate      = true;
            }
            else
            {
                classicUpdate = false;
            }

            targetPosPredictTimer += Time.deltaTime;
            if (targetPosPredictTimer >= predictionInterval)
            {
                targetPosPredictTimer = 0f;
                targetPosPredict      = true;
            }
            else
            {
                targetPosPredict = false;
            }

            // Reset whether enemy would be spawned or not in classic.
            if (classicUpdate)
            {
                wouldBeSpawnedInClassic = false;
            }

            // Update whether enemy would be spawned or not in classic.
            // Only check if within the maximum possible distance (Just under 1094 classic units)
            if (classicUpdate && distanceToPlayer < 1094 * MeshReader.GlobalScale)
            {
                float upperXZ      = 0;
                float upperY       = 0;
                float lowerY       = 0;
                bool  playerInside = GameManager.Instance.PlayerGPS.GetComponent <PlayerEnterExit>().IsPlayerInside;

                if (!playerInside)
                {
                    upperXZ = classicSpawnDespawnExterior;
                }
                else
                {
                    if (!wouldBeSpawnedInClassic)
                    {
                        upperXZ = classicSpawnXZDist;
                        upperY  = classicSpawnYDistUpper;
                        lowerY  = classicSpawnYDistLower;
                    }
                    else
                    {
                        upperXZ = classicDespawnXZDist;
                        upperY  = classicDespawnYDist;
                    }
                }

                float YDiffToPlayer      = transform.position.y - Player.transform.position.y;
                float YDiffToPlayerAbs   = Mathf.Abs(YDiffToPlayer);
                float distanceToPlayerXZ = Mathf.Sqrt(distanceToPlayer * distanceToPlayer - YDiffToPlayerAbs * YDiffToPlayerAbs);

                wouldBeSpawnedInClassic = true;

                if (distanceToPlayerXZ > upperXZ)
                {
                    wouldBeSpawnedInClassic = false;
                }

                if (playerInside)
                {
                    if (lowerY == 0)
                    {
                        if (YDiffToPlayerAbs > upperY)
                        {
                            wouldBeSpawnedInClassic = false;
                        }
                    }
                    else if (YDiffToPlayer < lowerY || YDiffToPlayer > upperY)
                    {
                        wouldBeSpawnedInClassic = false;
                    }
                }
            }

            if (classicUpdate)
            {
                classicTargetUpdateTimer += Time.deltaTime / systemTimerUpdatesDivisor;

                if (target != null && target.Entity.CurrentHealth <= 0)
                {
                    target = null;
                }

                // Non-hostile mode
                if (GameManager.Instance.PlayerEntity.NoTargetMode || !motor.IsHostile)
                {
                    if (target == Player)
                    {
                        target = null;
                    }
                    if (lastTarget == Player)
                    {
                        lastTarget = null;
                    }
                }

                // Reset these values if no target
                if (target == null)
                {
                    lastKnownTargetPos   = ResetPlayerPos;
                    predictedTargetPos   = ResetPlayerPos;
                    directionToTarget    = ResetPlayerPos;
                    lastDistanceToTarget = 0;
                    targetRateOfApproach = 0;
                    distanceToTarget     = 0;
                    targetSenses         = null;

                    // If had a valid target before, resume pursuing it. Looks better to first finish any attack animation.
                    if (lastTarget != null && lastTarget.Entity.CurrentHealth > 0 && !mobile.IsPlayingOneShot())
                    {
                        target = lastTarget;
                    }
                }

                if ((motor.IsHostile && target == null) || classicTargetUpdateTimer > 10) // Timing is 200 in classic, about 10 seconds.
                {
                    classicTargetUpdateTimer = 0f;

                    // Is enemy in area around player or can see player?
                    if (wouldBeSpawnedInClassic || playerInSight)
                    {
                        target = GetTarget();
                        if (target != null && target != Player)
                        {
                            targetSenses = target.GetComponent <EnemySenses>();
                        }
                        else
                        {
                            targetSenses = null;
                        }
                    }

                    // Make targeted character also target this character if it doesn't have a target yet.
                    if (target != null && targetSenses && targetSenses.Target == null)
                    {
                        targetSenses.Target = entityBehaviour;
                    }
                }

                // Compare change in target position to give AI some ability to read opponent's movements

                if (target != null && target == lastTarget)
                {
                    if (DaggerfallUnity.Settings.EnhancedCombatAI)
                    {
                        targetRateOfApproach = (lastDistanceToTarget - distanceToTarget);
                    }
                }
                else
                {
                    lastDistanceToTarget = 0;
                    targetRateOfApproach = 0;
                }

                if (target != null)
                {
                    lastDistanceToTarget = distanceToTarget;
                    lastTarget           = target;
                }
            }

            if (Player != null)
            {
                // Get distance to player
                Vector3 toPlayer = Player.transform.position - transform.position;
                distanceToPlayer = toPlayer.magnitude;

                // If out of classic spawn range, still check for direct LOS to player so that enemies who see player will
                // try to attack.
                if (!wouldBeSpawnedInClassic)
                {
                    distanceToTarget  = distanceToPlayer;
                    directionToTarget = toPlayer.normalized;
                    playerInSight     = CanSeeTarget(Player);
                }

                Vector3 toTarget = ResetPlayerPos;
                if (target != null)
                {
                    toTarget = target.transform.position - transform.position;
                }

                if (toTarget != ResetPlayerPos)
                {
                    distanceToTarget  = toTarget.magnitude;
                    directionToTarget = toTarget.normalized;
                }

                if (target == null)
                {
                    targetInSight  = false;
                    detectedTarget = false;
                    return;
                }

                targetInSight = CanSeeTarget(target);

                // Classic stealth mechanics would be interfered with by hearing, so only enable
                // hearing if the enemy has detected the target. If target is visible we can omit hearing.
                if (detectedTarget && !targetInSight)
                {
                    targetInEarshot = CanHearTarget(target);
                }
                else
                {
                    targetInEarshot = false;
                }

                // Note: In classic an enemy can continue to track the player as long as their
                // giveUpTimer is > 0. Since the timer is reset to 200 on every detection this
                // would make chameleon and shade essentially useless, since the enemy is sure
                // to detect the player during one of the many AI updates. Here, the enemy has to
                // successfully see through the illusion spell each classic update to continue
                // to know where the player is.
                if (classicUpdate)
                {
                    blockedByIllusionEffect = BlockedByIllusionEffect();
                    if (lastHadLOSTimer > 0)
                    {
                        lastHadLOSTimer--;
                    }
                }

                if (!blockedByIllusionEffect && (targetInSight || targetInEarshot))
                {
                    detectedTarget     = true;
                    lastKnownTargetPos = target.transform.position;
                    lastHadLOSTimer    = 200f;
                }
                else if (!blockedByIllusionEffect && StealthCheck())
                {
                    detectedTarget = true;

                    // Only get the target's location from the stealth check if we haven't had
                    // actual LOS for a while. This gives better pursuit behavior since enemies
                    // will go to the last spot they saw the player instead of walking into walls.
                    if (lastHadLOSTimer <= 0)
                    {
                        lastKnownTargetPos = target.transform.position;
                    }
                }
                else
                {
                    detectedTarget = false;
                }

                if (oldLastKnownTargetPos == ResetPlayerPos)
                {
                    oldLastKnownTargetPos = lastKnownTargetPos;
                }

                if (predictedTargetPos == ResetPlayerPos || !DaggerfallUnity.Settings.EnhancedCombatAI)
                {
                    predictedTargetPos = lastKnownTargetPos;
                }

                // Predict target's next position
                if (targetPosPredict && DaggerfallUnity.Settings.EnhancedCombatAI && predictedTargetPos != ResetPlayerPos && lastKnownTargetPos != ResetPlayerPos)
                {
                    float moveSpeed = (enemyEntity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale;
                    predictedTargetPos = PredictNextTargetPos(moveSpeed);
                }

                if (detectedTarget && !hasEncounteredPlayer && target == Player)
                {
                    hasEncounteredPlayer = true;

                    // Check appropriate language skill to see if player can pacify enemy
                    if (entityBehaviour && motor &&
                        (entityBehaviour.EntityType == EntityTypes.EnemyMonster || entityBehaviour.EntityType == EntityTypes.EnemyClass))
                    {
                        DFCareer.Skills languageSkill = enemyEntity.GetLanguageSkill();
                        if (languageSkill != DFCareer.Skills.None)
                        {
                            PlayerEntity player = GameManager.Instance.PlayerEntity;
                            if (FormulaHelper.CalculateEnemyPacification(player, languageSkill))
                            {
                                motor.IsHostile = false;
                                DaggerfallUI.AddHUDText(HardStrings.languagePacified.Replace("%e", enemyEntity.Name).Replace("%s", languageSkill.ToString()), 5);
                                player.TallySkill(languageSkill, 3);    // BCHG: increased skill uses from 1 in classic on success to make raising language skills easier
                            }
                            else if (languageSkill != DFCareer.Skills.Etiquette && languageSkill != DFCareer.Skills.Streetwise)
                            {
                                player.TallySkill(languageSkill, 1);
                            }
                        }
                    }
                }
            }
        }
Ejemplo n.º 6
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);
            }
        }
Ejemplo n.º 7
0
        private void Move()
        {
            // Do nothing if playing a one-shot animation
            if (mobile.IsPlayingOneShot())
            {
                return;
            }

            // Remain idle when player not acquired or not hostile
            if (senses.LastKnownPlayerPos == EnemySenses.ResetPlayerPos || !isHostile)
            {
                mobile.ChangeEnemyState(MobileStates.Idle);
                return;
            }

            // Enemy will keep moving towards last known player position
            targetPos = senses.LastKnownPlayerPos;
            if (targetPos == lastTargetPos)
            {
                // Increment countdown to giving up when target is uncreachable and player lost
                giveUpTimer += Time.deltaTime;
                if (giveUpTimer > GiveUpTime &&
                    !senses.PlayerInSight && !senses.PlayerInEarshot)
                {
                    // Target is unreachable or player lost for too long, time to give up
                    senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                    return;
                }
            }
            else
            {
                // Still chasing, update last target and reset give up timer
                lastTargetPos = targetPos;
                giveUpTimer   = 0;
            }

            // Get distance to target
            float distance = Vector3.Distance(targetPos, transform.position);

            // Flying enemies aim for player face
            if (mobile.Summary.Enemy.Behaviour == MobileBehaviour.Flying ||
                mobile.Summary.Enemy.Behaviour == MobileBehaviour.Spectral)
            {
                targetPos.y += 0.9f;
            }

            // Get direction and face target
            Vector3 direction = targetPos - transform.position;

            transform.forward = direction.normalized;

            // Move towards target
            if (distance > stopDistance)
            {
                mobile.ChangeEnemyState(MobileStates.Move);
                if (mobile.Summary.Enemy.Behaviour == MobileBehaviour.Flying ||
                    mobile.Summary.Enemy.Behaviour == MobileBehaviour.Spectral)
                {
                    controller.Move(transform.forward * (FlySpeed * Time.deltaTime));
                }
                else
                {
                    controller.SimpleMove(transform.forward * ((MoveSpeed * 40f) * Time.deltaTime));    // Not sure why SimpleMove() needs to be scaled. Check this.
                }
            }
            else
            {
                // We have reached target, is player nearby?
                if (!senses.PlayerInSight && !senses.PlayerInEarshot)
                {
                    senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                }
            }
        }
Ejemplo n.º 8
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);
            }
        }
Ejemplo n.º 9
0
        private void Move()
        {
            // Do nothing if playing a one-shot animation
            if (mobile.IsPlayingOneShot())
            {
                return;
            }

            // Remain idle when player not acquired or not hostile
            if (senses.LastKnownPlayerPos == EnemySenses.ResetPlayerPos || !isHostile)
            {
                mobile.ChangeEnemyState(MobileStates.Idle);
                return;
            }

            // Enemy will keep moving towards last known player position
            targetPos = senses.LastKnownPlayerPos;
            if (targetPos == lastTargetPos)
            {
                // Increment countdown to giving up when target is uncreachable and player lost
                giveUpTimer += Time.deltaTime;
                if (giveUpTimer > GiveUpTime &&
                    !senses.PlayerInSight && !senses.PlayerInEarshot)
                {
                    // Target is unreachable or player lost for too long, time to give up
                    senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                    return;
                }
            }
            else
            {
                // Still chasing, update last target and reset give up timer
                lastTargetPos = targetPos;
                giveUpTimer   = 0;
            }

            // Flying enemies aim for player face
            if (flies)
            {
                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 = senses.Player.GetComponent <CharacterController>();
                var deltaHeight      = (playerController.height - controller.height) / 2;
                targetPos.y -= deltaHeight;
            }

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

            transform.forward = direction.normalized;

            // Move towards target
            if (distance > stopDistance)
            {
                mobile.ChangeEnemyState(MobileStates.Move);
                var motion = transform.forward * (flies ? FlySpeed : MoveSpeed);

                // Prevent rat stacks (enemies don't stand on shorter enemies)
                AvoidEnemies(ref motion);

                if (flies)
                {
                    controller.Move(motion * Time.deltaTime);
                }
                else
                {
                    controller.SimpleMove(motion);
                }
            }
            else
            {
                // We have reached target, is player nearby?
                if (!senses.PlayerInSight && !senses.PlayerInEarshot)
                {
                    senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                }
            }
        }
Ejemplo n.º 10
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;
            }
        }
Ejemplo n.º 11
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;
            }
        }
Ejemplo n.º 12
0
        private void Move()
        {
            // Monster speed of movement follows the same formula as for when the player walks
            EnemyEntity entity    = entityBehaviour.Entity as EnemyEntity;
            float       moveSpeed = ((entity.Stats.Speed + PlayerMotor.dfWalkBase) / PlayerMotor.classicToUnitySpeedUnitRatio);

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

            // Remain idle when player not acquired or not hostile
            if (senses.LastKnownPlayerPos == EnemySenses.ResetPlayerPos || !isHostile)
            {
                mobile.ChangeEnemyState(MobileStates.Idle);
                return;
            }

            // Enemy will keep moving towards last known player position
            targetPos = senses.LastKnownPlayerPos;
            if (targetPos == lastTargetPos)
            {
                // Increment countdown to giving up when target is uncreachable and player lost
                giveUpTimer += Time.deltaTime;
                if (giveUpTimer > GiveUpTime &&
                    !senses.PlayerInSight && !senses.PlayerInEarshot)
                {
                    // Target is unreachable or player lost for too long, time to give up
                    senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                    return;
                }
            }
            else
            {
                // Still chasing, update last target and reset give up timer
                lastTargetPos = targetPos;
                giveUpTimer   = 0;
            }

            // Flying enemies aim for player face
            if (flies)
            {
                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 and face target.
            // If attacking, randomly do not do so so player has a chance to see
            // attack animations other than those directly facing the player
            var   direction = targetPos - transform.position;
            float distance  = direction.magnitude;

            if (mobile.IsPlayingOneShot() && !isAttackFollowsPlayerSet)
            {
                attackFollowsPlayer      = (Random.Range(0f, 1f) > 0.5f);
                isAttackFollowsPlayerSet = true;
            }
            else if (!mobile.IsPlayingOneShot())
            {
                isAttackFollowsPlayerSet = false;
            }

            if (!mobile.IsPlayingOneShot() || !attackFollowsPlayer)
            {
                transform.forward = direction.normalized;
            }

            // Move towards target
            if (distance > stopDistance)
            {
                if (!mobile.IsPlayingOneShot())
                {
                    mobile.ChangeEnemyState(MobileStates.Move);
                }
                var motion = transform.forward * moveSpeed;

                // Prevent rat stacks (enemies don't stand on shorter enemies)
                AvoidEnemies(ref motion);

                if (flies)
                {
                    controller.Move(motion * Time.deltaTime);
                }
                else
                {
                    controller.SimpleMove(motion);
                }
            }
            else
            {
                // We have reached target, is player nearby?
                if (!senses.PlayerInSight && !senses.PlayerInEarshot)
                {
                    senses.LastKnownPlayerPos = EnemySenses.ResetPlayerPos;
                }
            }
        }