/// <summary> /// Make decision about what movement action to take. /// </summary> void Move() { // Cancel movement and animations if paralyzed, but still allow gravity to take effect // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air // Paralyzed swimming enemies will just freeze in place // Freezing anims also prevents the attack from triggering until paralysis cleared if (entityBehaviour.Entity.IsParalyzed) { mobile.FreezeAnims = true; if ((swims || flies) && !isLevitating) { controller.Move(Vector3.zero); } else { controller.SimpleMove(Vector3.zero); } return; } mobile.FreezeAnims = false; // Apply gravity to non-moving AI if active (has a combat target) or nearby if ((entityBehaviour.Target != null || senses.WouldBeSpawnedInClassic) && !flies && !swims) { controller.SimpleMove(Vector3.zero); } // If hit, get knocked back if (knockBackSpeed > 0) { // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion, // making it last longer and do more damage if the enemy collides with something (TODO). if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Hurt); } // Actual speed of motion is limited Vector3 motion; if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { motion = knockBackDirection * knockBackSpeed; } else { motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } // Move in direction of knockback if (swims) { WaterMove(motion); } else if (flies || isLevitating) { controller.Move(motion * Time.deltaTime); } else { controller.SimpleMove(motion); } // Remove remaining knockback and restore animation if (classicUpdate) { knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Move); } } // If a decent hit got in, reconsider whether to continue current tactic if (knockBackSpeed > (10 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { EvaluateMoveInForAttack(); } return; } // Monster speed of movement follows the same formula as for when the player walks float moveSpeed = (entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale; // Reduced speed if playing a one-shot animation with enhanced AI if (mobile.IsPlayingOneShot() && DaggerfallUnity.Settings.EnhancedCombatAI) { moveSpeed /= AttackSpeedDivisor; } // As long as the target is detected, // giveUpTimer is reset to full if (senses.DetectedTarget) { giveUpTimer = 200; } // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop if (!senses.DetectedTarget && giveUpTimer > 0 && classicUpdate) { giveUpTimer--; } // Change to idle animation if haven't moved or rotated if (!mobile.IsPlayingOneShot()) { // Rotation is done at classic update rate, so check at classic update rate if (classicUpdate) { Vector3 currentDirection = transform.forward; currentDirection.y = 0; if (lastPosition == transform.position && lastDirection == currentDirection) { mobile.ChangeEnemyState(MobileStates.Idle); rotating = false; } else { mobile.ChangeEnemyState(MobileStates.Move); } lastDirection = currentDirection; } // Movement is done at regular update rate, so check at regular update rate else if (!rotating && lastPosition == transform.position) { mobile.ChangeEnemyState(MobileStates.Idle); } else { mobile.ChangeEnemyState(MobileStates.Move); } lastPosition = transform.position; } // Do nothing if no target or after giving up finding the target if (entityBehaviour.Target == null || giveUpTimer == 0) { SetChangeStateTimer(); return; } // Get predicted target position if (avoidObstaclesTimer == 0 && !lookingForDetour) { targetPos = senses.PredictedTargetPos; // Flying enemies and slaughterfish aim for target face if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish)) { targetPos.y += 0.9f; } else { // Ground enemies target at their own height // This avoids short enemies from stepping on each other as they approach the target // Otherwise, their target vector aims up towards the target var playerController = GameManager.Instance.PlayerController; var deltaHeight = (playerController.height - controller.height) / 2; targetPos.y -= deltaHeight; } tempMovePos = targetPos; } else { targetPos = tempMovePos; } // Get direction & distance. var direction = targetPos - transform.position; float distance = (targetPos - transform.position).magnitude; // Ranged attacks if (senses.TargetInSight && 360 * MeshReader.GlobalScale < senses.DistanceToTarget && senses.DistanceToTarget < 2048 * MeshReader.GlobalScale) { bool evaluateBow = mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132; bool evaluateRangedMagic = false; if (!evaluateBow) { evaluateRangedMagic = CanCastRangedSpell(); } if (evaluateBow || evaluateRangedMagic) { if (senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos)) { if (!mobile.IsPlayingOneShot()) { if (evaluateBow) { // Random chance to shoot bow if (classicUpdate && DFRandom.rand() < 1000) { if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack1); } else if (mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack2); } } } // Random chance to shoot spell else if (classicUpdate && DFRandom.rand() % 40 == 0 && entityEffectManager.SetReadySpell(selectedSpell)) { mobile.ChangeEnemyState(MobileStates.Spell); } } } else { TurnToTarget(direction.normalized); } return; } } if (senses.TargetInSight && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance + senses.TargetRateOfApproach && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell)) { if (mobile.Summary.EnemyState != MobileStates.Spell) { mobile.ChangeEnemyState(MobileStates.Spell); } attack.ResetMeleeTimer(); return; } // Update melee decision if (moveInForAttackTimer <= 0 && avoidObstaclesTimer == 0 && !lookingForDetour) { EvaluateMoveInForAttack(); } if (moveInForAttackTimer > 0) { moveInForAttackTimer -= Time.deltaTime; } if (avoidObstaclesTimer > 0) { avoidObstaclesTimer -= Time.deltaTime; } if (avoidObstaclesTimer < 0) { avoidObstaclesTimer = 0; } if (changeStateTimer > 0) { changeStateTimer -= Time.deltaTime; } // Looking for detour if (lookingForDetour) { CombatMove(direction, moveSpeed); } // Approach target until we are close enough to be on-guard, or continue to melee range if attacking else if ((!retreating && distance >= (stopDistance * 2.75)) || (distance > stopDistance && moveInForAttack)) { // If state change timer is done, or we are already pursuing, we can move if (changeStateTimer <= 0 || pursuing) { CombatMove(direction, moveSpeed); } // Otherwise, just keep an eye on target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } } // Back away if right next to target, if retreating, or if cooling down from attack // Classic AI never backs away else if (DaggerfallUnity.Settings.EnhancedCombatAI && (senses.TargetInSight && (distance < stopDistance * .50 || (!moveInForAttack && distance < (stopDistance * retreatDistanceMultiplier))))) { // If state change timer is done, or we are already retreating, we can move if (changeStateTimer <= 0 || retreating) { CombatMove(direction, moveSpeed / 2, true); } // Otherwise, just keep an eye on target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } } else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } else if (avoidObstaclesTimer > 0 && distance > 0.1f) { CombatMove(direction, moveSpeed); } else // Next to target { SetChangeStateTimer(); pursuing = false; retreating = false; avoidObstaclesTimer = 0; } }
/// <summary> /// Make decision about what movement action to take. /// </summary> void Move() { // Cancel movement and animations if paralyzed, but still allow gravity to take effect // This will have the (intentional for now) side-effect of making paralyzed flying enemies fall out of the air // Paralyzed swimming enemies will just freeze in place // Freezing anims also prevents the attack from triggering until paralysis cleared if (entityBehaviour.Entity.IsParalyzed) { mobile.FreezeAnims = true; if ((swims || flies) && !isLevitating) { controller.Move(Vector3.zero); } else { controller.SimpleMove(Vector3.zero); } return; } mobile.FreezeAnims = false; // If hit, get knocked back if (knockBackSpeed > 0) { // Limit knockBackSpeed. This can be higher than what is actually used for the speed of motion, // making it last longer and do more damage if the enemy collides with something (TODO). if (knockBackSpeed > (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { knockBackSpeed = (40 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } if (knockBackSpeed > (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Hurt); } // Actual speed of motion is limited Vector3 motion; if (knockBackSpeed <= (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { motion = knockBackDirection * knockBackSpeed; } else { motion = knockBackDirection * (25 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); } // Move in direction of knockback if (swims) { WaterMove(motion); } else if (flies || isLevitating) { controller.Move(motion * Time.deltaTime); } else { controller.SimpleMove(motion); } // Remove remaining knockback and restore animation if (classicUpdate) { knockBackSpeed -= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)); if (knockBackSpeed <= (5 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10)) && mobile.Summary.EnemyState != MobileStates.PrimaryAttack) { mobile.ChangeEnemyState(MobileStates.Move); } } // If a decent hit got in, reconsider whether to continue current tactic if (knockBackSpeed > (10 / (PlayerSpeedChanger.classicToUnitySpeedUnitRatio / 10))) { EvaluateMoveInForAttack(); } return; } // Apply gravity if (!flies && !swims && !isLevitating && !controller.isGrounded) { controller.SimpleMove(Vector3.zero); // Only return if actually falling. Sometimes mobiles can get stuck where they are !isGrounded but SimpleMove(Vector3.zero) doesn't help. // Allowing them to continue and attempt a Move() in the code below frees them, but we don't want to allow that if we can avoid it so they aren't moving // while falling, which can also accelerate the fall due to anti-bounce downward movement in Move(). if (lastPosition != transform.position) { return; } } // Monster speed of movement follows the same formula as for when the player walks float moveSpeed = (entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) * MeshReader.GlobalScale; // Get isPlayingOneShot for use below bool isPlayingOneShot = mobile.IsPlayingOneShot(); // Reduced speed if playing a one-shot animation with enhanced AI if (isPlayingOneShot && DaggerfallUnity.Settings.EnhancedCombatAI) { moveSpeed /= AttackSpeedDivisor; } // As long as the target is detected, // giveUpTimer is reset to full if (senses.DetectedTarget) { giveUpTimer = 200; } // GiveUpTimer value is from classic, so decrease at the speed of classic's update loop if (classicUpdate && !senses.DetectedTarget && giveUpTimer > 0) { giveUpTimer--; } // Change to idle animation if haven't moved or rotated if (!mobile.IsPlayingOneShot()) { // Rotation is done at classic update rate, so check at classic update rate if (classicUpdate) { Vector3 currentDirection = transform.forward; currentDirection.y = 0; if (lastPosition == transform.position && lastDirection == currentDirection) { mobile.ChangeEnemyState(MobileStates.Idle); rotating = false; } else { mobile.ChangeEnemyState(MobileStates.Move); } lastDirection = currentDirection; } // Movement is done at regular update rate, so check at regular update rate else if (!rotating && lastPosition == transform.position) { mobile.ChangeEnemyState(MobileStates.Idle); } else { mobile.ChangeEnemyState(MobileStates.Move); } lastPosition = transform.position; } // Do nothing if no target or after giving up finding the target or if target position hasn't been acquired yet if (senses.Target == null || giveUpTimer == 0 || senses.PredictedTargetPos == EnemySenses.ResetPlayerPos) { SetChangeStateTimer(); bashing = false; return; } if (bashing) { if (senses.TargetInSight || senses.LastKnownDoor == null || !senses.LastKnownDoor.IsLocked) { bashing = false; } else { int speed = entity.Stats.LiveSpeed; if (classicUpdate && DFRandom.rand() % speed >= (speed >> 3) + 6 && attack.MeleeTimer == 0) { mobile.ChangeEnemyState(MobileStates.PrimaryAttack); attack.ResetMeleeTimer(); } return; } } bool targetPosIsEnemyPos = false; // Get location to move towards. Either the combat target's position or, if trying to avoid an obstacle or fall, // a location to try to detour around the obstacle/fall. if (avoidObstaclesTimer == 0 && (senses.PredictedTargetPos.y > transform.position.y || ClearPathToPosition(senses.PredictedTargetPos))) { targetPos = senses.PredictedTargetPos; // Flying enemies and slaughterfish aim for target face if (flies || isLevitating || (swims && mobile.Summary.Enemy.ID == (int)MonsterCareers.Slaughterfish)) { targetPos.y += 0.9f; } else { // Ground enemies target at their own height // This avoids short enemies from stepping on each other as they approach the target // Otherwise, their target vector aims up towards the target var targetController = senses.Target.GetComponent <CharacterController>(); var deltaHeight = (targetController.height - controller.height) / 2; targetPos.y -= deltaHeight; } tempMovePos = targetPos; targetPosIsEnemyPos = true; } // If detouring, use the detour position else if (avoidObstaclesTimer > 0) { targetPos = tempMovePos; } // Otherwise, go straight else { tempMovePos = transform.position + transform.forward * 2; targetPos = tempMovePos; } // Get direction & distance. var direction = (targetPos - transform.position).normalized; float distance = (targetPos - transform.position).magnitude; // Ranged attacks if (targetPosIsEnemyPos && senses.DetectedTarget && 360 * MeshReader.GlobalScale < senses.DistanceToTarget && senses.DistanceToTarget < 2048 * MeshReader.GlobalScale) { bool evaluateBow = mobile.Summary.Enemy.HasRangedAttack1 && mobile.Summary.Enemy.ID > 129 && mobile.Summary.Enemy.ID != 132; bool evaluateRangedMagic = false; if (!evaluateBow) { evaluateRangedMagic = CanCastRangedSpell(); } if (evaluateBow || evaluateRangedMagic) { if (classicUpdate && senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos)) { if (!isPlayingOneShot) { if (evaluateBow) { // Random chance to shoot bow if (DFRandom.rand() < 1000) { if (mobile.Summary.Enemy.HasRangedAttack1 && !mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack1); } else if (mobile.Summary.Enemy.HasRangedAttack2) { mobile.ChangeEnemyState(MobileStates.RangedAttack2); } } } // Random chance to shoot spell else if (DFRandom.rand() % 40 == 0 && entityEffectManager.SetReadySpell(selectedSpell)) { mobile.ChangeEnemyState(MobileStates.Spell); } } } else { TurnToTarget(direction); } return; } } // Touch spells if (targetPosIsEnemyPos && senses.TargetInSight && senses.DetectedTarget && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance + senses.TargetRateOfApproach && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell)) { if (mobile.Summary.EnemyState != MobileStates.Spell) { mobile.ChangeEnemyState(MobileStates.Spell); } attack.ResetMeleeTimer(); return; } // Update advance/retreat decision if (moveInForAttackTimer <= 0 && avoidObstaclesTimer == 0) { EvaluateMoveInForAttack(); } // Update timers if (moveInForAttackTimer > 0) { moveInForAttackTimer -= Time.deltaTime; } if (avoidObstaclesTimer > 0 && senses.TargetIsWithinYawAngle(5.625f, targetPos)) { avoidObstaclesTimer -= Time.deltaTime; } if (avoidObstaclesTimer < 0) { avoidObstaclesTimer = 0; } if (checkingClockwiseTimer > 0) { checkingClockwiseTimer -= Time.deltaTime; } if (checkingClockwiseTimer < 0) { checkingClockwiseTimer = 0; } if (changeStateTimer > 0) { changeStateTimer -= Time.deltaTime; } // If detouring, attempt to move if (avoidObstaclesTimer > 0) { AttemptMove(direction, moveSpeed); } // Otherwise, if not still executing a retreat, approach target until close enough to be on-guard. // If decided to move in for attack, continue until within melee range. Classic always moves in for attack. else if ((!retreating && distance >= (stopDistance * 2.75)) || (distance > stopDistance && moveInForAttack)) { // If state change timer is done, or we are continuing an already started combatMove, we can move immediately if (changeStateTimer <= 0 || pursuing) { AttemptMove(direction, moveSpeed); } // Otherwise, look at target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction); } } // Back away from combat target if right next to it, or if decided to retreat and enemy is too close. // Classic AI never backs awwy. else if (DaggerfallUnity.Settings.EnhancedCombatAI && senses.TargetInSight && (distance < stopDistance * .50 || (!moveInForAttack && distance < (stopDistance * retreatDistanceMultiplier)))) { // If state change timer is done, or we are already executing a retreat, we can move immediately if (changeStateTimer <= 0 || retreating) { AttemptMove(direction, moveSpeed / 2, true); } // Otherwise, look at target until timer finishes else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } } // Not moving, just look at target else if (!senses.TargetIsWithinYawAngle(22.5f, targetPos)) { TurnToTarget(direction.normalized); } else // Not moving, and no need to turn { SetChangeStateTimer(); pursuing = false; retreating = false; avoidObstaclesTimer = 0; } }