private void MeleeAnimation() { // Are we in range and facing player? Then start attack. if (senses.PlayerInSight) { // Take the speed of movement during the attack animation into account when deciding if to attack EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; float attackSpeed = ((entity.Stats.LiveSpeed + PlayerSpeedChanger.dfWalkBase) / PlayerSpeedChanger.classicToUnitySpeedUnitRatio) / EnemyMotor.AttackSpeedDivisor; if (senses.DistanceToPlayer >= MeleeDistance + attackSpeed) { return; } // Don't attack if not hostile if (!motor.IsHostile) { return; } // Set melee animation state mobile.ChangeEnemyState(MobileStates.PrimaryAttack); // Play melee sound if (sounds) { sounds.PlayAttackSound(); } } }
private bool MeleeAnimation() { // Are we in range and facing target? Then start attack. if (senses.TargetInSight && senses.TargetIsWithinYawAngle(22.5f, senses.LastKnownTargetPos)) { float distance = MeleeDistance; // Classic uses separate melee distance for targeting player and for targeting other AI if (!DaggerfallUnity.Settings.EnhancedCombatAI && senses.Target != GameManager.Instance.PlayerEntityBehaviour) { distance = ClassicMeleeDistanceVsAI; } // Take the rate of target approach into account when deciding if to attack if (senses.DistanceToTarget > distance + senses.TargetRateOfApproach) { return(false); } // Set melee animation state mobile.ChangeEnemyState(MobileStates.PrimaryAttack); // Play melee sound if (sounds) { sounds.PlayAttackSound(); } return(true); } return(false); }
private bool MeleeAnimation() { // Are we in range and facing target? Then start attack. if (senses.TargetInSight) { // Take the rate of target approach into account when deciding if to attack if (senses.DistanceToTarget >= MeleeDistance + senses.TargetRateOfApproach) { return(false); } // Set melee animation state mobile.ChangeEnemyState(MobileStates.PrimaryAttack); // Play melee sound if (sounds) { sounds.PlayAttackSound(); } return(true); } return(false); }
private void MeleeAnimation() { // Are we in range and facing player? Then start attack. if (senses.PlayerInSight) { // Take the speed of movement during the attack animation and hit frame into account when calculating attack range EnemyEntity entity = entityBehaviour.Entity as EnemyEntity; float attackSpeed = ((entity.Stats.LiveSpeed + PlayerMotor.dfWalkBase) / PlayerMotor.classicToUnitySpeedUnitRatio) / EnemyMotor.AttackSpeedDivisor; float timeUntilHit = mobile.Summary.Enemy.HitFrame / DaggerfallWorkshop.Utility.EnemyBasics.PrimaryAttackAnimSpeed; if (senses.DistanceToPlayer >= (MeleeDistance + (attackSpeed * timeUntilHit))) { return; } // Don't attack if not hostile if (!motor.IsHostile) { return; } // Set melee animation state mobile.ChangeEnemyState(MobileStates.PrimaryAttack); // Play melee sound if (sounds) { sounds.PlayAttackSound(); } } }
private void MeleeAnimation() { // Are we in range and facing player? Then start attack. if (senses.DistanceToPlayer < MeleeDistance && senses.PlayerInSight) { // Set melee animation state mobile.ChangeEnemyState(MobileStates.PrimaryAttack); // Play melee sound if (sounds) { sounds.PlayAttackSound(); } } }
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); } }
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; } } }
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); } }
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; } } }
/// <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; } }
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; } } }