diff --git a/Scenes/Actors/Fairy_FSM.tscn b/Scenes/Actors/Fairy_FSM.tscn index 5de4d9c5..47706d22 100644 --- a/Scenes/Actors/Fairy_FSM.tscn +++ b/Scenes/Actors/Fairy_FSM.tscn @@ -108,11 +108,12 @@ DamageReceiver = NodePath("../../DamageReceiver") NavigationModule = NodePath("../../NavigationModule") _moduleNodes = [NodePath("../../AnimationModule")] -[node name="Shooting" type="Node2D" parent="StateMachine" node_paths=PackedStringArray("StorageModule", "PlayerDetection", "DamageReceiver", "EquippedWeapon", "_moduleNodes")] +[node name="Shooting" type="Node2D" parent="StateMachine" node_paths=PackedStringArray("StorageModule", "PlayerDetection", "DamageReceiver", "NavigationModule", "EquippedWeapon", "_moduleNodes")] script = ExtResource("7_br0mr") StorageModule = NodePath("../../Storage") PlayerDetection = NodePath("../../PlayerDetection") DamageReceiver = NodePath("../../DamageReceiver") +NavigationModule = NodePath("../../NavigationModule") EquippedWeapon = NodePath("../../EnemyWeapon") _moduleNodes = [NodePath("../../AnimationModule")] diff --git a/Scenes/Actors/Fairy_Guard_FSM.tscn b/Scenes/Actors/Fairy_Guard_FSM.tscn index 4d9490f3..271a84c0 100644 --- a/Scenes/Actors/Fairy_Guard_FSM.tscn +++ b/Scenes/Actors/Fairy_Guard_FSM.tscn @@ -108,11 +108,12 @@ DamageReceiver = NodePath("../../DamageReceiver") NavigationModule = NodePath("../../NavigationModule") _moduleNodes = [NodePath("../../AnimationModule")] -[node name="Shooting" type="Node2D" parent="StateMachine" node_paths=PackedStringArray("StorageModule", "PlayerDetection", "DamageReceiver", "EquippedWeapon", "_moduleNodes")] +[node name="Shooting" type="Node2D" parent="StateMachine" node_paths=PackedStringArray("StorageModule", "PlayerDetection", "DamageReceiver", "NavigationModule", "EquippedWeapon", "_moduleNodes")] script = ExtResource("7_w6ssf") StorageModule = NodePath("../../Storage") PlayerDetection = NodePath("../../PlayerDetection") DamageReceiver = NodePath("../../DamageReceiver") +NavigationModule = NodePath("../../NavigationModule") EquippedWeapon = NodePath("../../EnemyWeapon") _moduleNodes = [NodePath("../../AnimationModule")] @@ -173,6 +174,7 @@ target_desired_distance = 8.0 path_max_distance = 800.0 path_postprocessing = 1 avoidance_enabled = true +debug_enabled = true debug_path_custom_color = Color(1, 0, 0, 1) [node name="EnemyWeapon" parent="." instance=ExtResource("17_i8o8t")] diff --git a/Scenes/Actors/Thermatron_FSM.tscn b/Scenes/Actors/Thermatron_FSM.tscn index 3ee27e2b..a207d8f6 100644 --- a/Scenes/Actors/Thermatron_FSM.tscn +++ b/Scenes/Actors/Thermatron_FSM.tscn @@ -108,11 +108,12 @@ DamageReceiver = NodePath("../../DamageReceiver") NavigationModule = NodePath("../../NavigationModule") _moduleNodes = [NodePath("../../AnimationModule")] -[node name="Shooting" type="Node2D" parent="StateMachine" node_paths=PackedStringArray("StorageModule", "PlayerDetection", "DamageReceiver", "EquippedWeapon", "_moduleNodes")] +[node name="Shooting" type="Node2D" parent="StateMachine" node_paths=PackedStringArray("StorageModule", "PlayerDetection", "DamageReceiver", "NavigationModule", "EquippedWeapon", "_moduleNodes")] script = ExtResource("7_0b4ec") StorageModule = NodePath("../../Storage") PlayerDetection = NodePath("../../PlayerDetection") DamageReceiver = NodePath("../../DamageReceiver") +NavigationModule = NodePath("../../NavigationModule") EquippedWeapon = NodePath("../../EnemyWeapon") _moduleNodes = [NodePath("../../AnimationModule")] diff --git a/Scripts/Components/FSM/Enemy/NavigationMovementModule.cs b/Scripts/Components/FSM/Enemy/NavigationMovementModule.cs index 72980a6f..874f790a 100644 --- a/Scripts/Components/FSM/Enemy/NavigationMovementModule.cs +++ b/Scripts/Components/FSM/Enemy/NavigationMovementModule.cs @@ -66,4 +66,15 @@ public partial class NavigationMovementModule : Node2D StorageModule.FacingDirection = _characterBody.Velocity.Normalized(); } } + + public bool IsNavigable(Vector2 newPos) + { + _navigationAgent.SetTargetPosition(newPos); + return _navigationAgent.IsTargetReachable(); + } + + public bool IsNavigationFinished() + { + return _navigationAgent.IsNavigationFinished(); + } } \ No newline at end of file diff --git a/Scripts/Components/FSM/Enemy/PlayerDetectionModule.cs b/Scripts/Components/FSM/Enemy/PlayerDetectionModule.cs index 69baeb6b..480250f3 100644 --- a/Scripts/Components/FSM/Enemy/PlayerDetectionModule.cs +++ b/Scripts/Components/FSM/Enemy/PlayerDetectionModule.cs @@ -60,18 +60,20 @@ public partial class PlayerDetectionModule : Area2D //if (_cachedPlayer == null) return false; if (!GameManager.Instance.PlayerPosition.HasValue) return false; - var spaceState = GetWorld2D().DirectSpaceState; + var found = HasLineOfSight(this.GlobalPosition, GameManager.Instance.PlayerPosition.Value); - // It needs to use its own collision mask because it's detecting obstacles rather than the player - var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, GameManager.Instance.PlayerPosition.Value, ObstaclesCollisionMask, - [GetRid()]); - //query.CollideWithBodies = true; - //query.CollideWithAreas = true; - // var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array { GetRid() }); - var result = spaceState.IntersectRay(query); - - // If count is 0 then the player is in sight, otherwise there is level geometry in the way - var found = result.Count == 0; + // var spaceState = GetWorld2D().DirectSpaceState; + // + // // It needs to use its own collision mask because it's detecting obstacles rather than the player + // var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, GameManager.Instance.PlayerPosition.Value, ObstaclesCollisionMask, + // [GetRid()]); + // //query.CollideWithBodies = true; + // //query.CollideWithAreas = true; + // // var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array { GetRid() }); + // var result = spaceState.IntersectRay(query); + // + // // If count is 0 then the player is in sight, otherwise there is level geometry in the way + // var found = result.Count == 0; if (found) { LastKnownPlayerPosition = GameManager.Instance.PlayerPosition; @@ -92,4 +94,22 @@ public partial class PlayerDetectionModule : Area2D EmitSignal(SignalName.PlayerOutOfRange); //PlayerInActiveArea = false; } + + public bool HasLineOfSight(Vector2 startPos, Vector2 endPos) + { + var spaceState = GetWorld2D().DirectSpaceState; + + // It needs to use its own collision mask because it's detecting obstacles rather than the player + var query = PhysicsRayQueryParameters2D.Create(startPos, endPos, ObstaclesCollisionMask, + [GetRid()]); + //query.CollideWithBodies = true; + //query.CollideWithAreas = true; + // var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array { GetRid() }); + var result = spaceState.IntersectRay(query); + + // If count is 0 then the player is in sight, otherwise there is level geometry in the way + var found = result.Count == 0; + + return found; + } } \ No newline at end of file diff --git a/Scripts/Components/FSM/Enemy/Shooting.cs b/Scripts/Components/FSM/Enemy/Shooting.cs index 89e2ff8a..c6f52454 100644 --- a/Scripts/Components/FSM/Enemy/Shooting.cs +++ b/Scripts/Components/FSM/Enemy/Shooting.cs @@ -17,10 +17,18 @@ public partial class Shooting : EnemyStateBase [Export] public GenericDamageReceiver DamageReceiver { get; private set; } + [Export] + public NavigationMovementModule NavigationModule { get; private set; } + + [Export] public float MaxStrafeDistance { get; private set; } = 64f; + [Export] public float MinStrafeDistance { get; private set; } = 16f; + [Export] public Weapon EquippedWeapon; private bool _isPlayerInRange = false; + private Vector2? _currentStrafeTarget = null; + public override void EnterState() { base.EnterState(); @@ -35,6 +43,8 @@ public partial class Shooting : EnemyStateBase DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted; EquippedWeapon.WeaponData = StorageModule.Root.EnemyResource.Weapon; + + _currentStrafeTarget = null; // PlayerDetection.SetRange(StorageModule.Root.EnemyResource.PlayerDetectionRange); // // _isPlayerInRange = PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.PlayerDetectionRange); @@ -67,6 +77,9 @@ public partial class Shooting : EnemyStateBase PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange; DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted; + + _currentStrafeTarget = null; + DamageReceiver.ChangeState(false); } @@ -79,14 +92,62 @@ public partial class Shooting : EnemyStateBase { // SHOOT Shoot(); + + // Check if a strafe position is needed + if (!_currentStrafeTarget.HasValue || NavigationModule.IsNavigationFinished()) + { + _currentStrafeTarget = CalculateStrafePosition(); + } } else { StateMachine.SetState(EnemyState.Alert); + return; } - // Strafe - + if (_currentStrafeTarget.HasValue) + { + NavigationModule.SetTarget(_currentStrafeTarget.Value); + NavigationModule.Move(); + } + } + + private Vector2? CalculateStrafePosition() + { + Vector2 playerPos = PlayerDetection.LastKnownPlayerPosition.Value; + Vector2 enemyPos = MainObject.GlobalPosition; + + // Calculate direction to player + Vector2 directionToPlayer = (playerPos - enemyPos).Normalized(); + + // Get perpendicular vectors (left and right strafing directions) + Vector2 leftStrafe = directionToPlayer.Rotated(-Mathf.Pi / 2); + Vector2 rightStrafe = directionToPlayer.Rotated(Mathf.Pi / 2); + + // Randomly decide left or right first + bool tryLeftFirst = GD.Randf() < 0.5f; + + for (float factor = 1f; factor > 0.25f; factor *= 0.75f) + { + float strafeDistance = Mathf.Lerp(MinStrafeDistance, MaxStrafeDistance, factor); + Vector2 newPos = enemyPos + (tryLeftFirst ? leftStrafe : rightStrafe) * strafeDistance; + if (NavigationModule.IsNavigable(newPos) && PlayerDetection.HasLineOfSight(newPos, playerPos)) + { + return newPos; + } + } + + for (float factor = 1f; factor > 0.25f; factor *= 0.75f) + { + float strafeDistance = Mathf.Lerp(MinStrafeDistance, MaxStrafeDistance, factor); + Vector2 newPos = enemyPos + (tryLeftFirst ? rightStrafe : leftStrafe) * strafeDistance; + if (NavigationModule.IsNavigable(newPos) && PlayerDetection.HasLineOfSight(newPos, playerPos)) + { + return newPos; + } + } + + return null; // No valid strafe position found } private void Shoot()