using Cirno.Scripts.Components.Actors; using Cirno.Scripts.Enums; using Cirno.Scripts.Utils; using Godot; namespace Cirno.Scripts.Components.FSM.Enemy; public partial class Shooting : EnemyStateBase { public override EnemyState StateId => EnemyState.Shooting; [Export] public EnemyStorageModule StorageModule { get; private set; } [Export] public PlayerDetectionModule PlayerDetection { get; private set; } [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; private float _strafeSpeed => StorageModule.EnemyData.StrafeSpeed; private double _responseTimer = 0; public override void EnterState() { base.EnterState(); PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange; DamageReceiver.ChangeState(true); DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted; EquippedWeapon.WeaponData = StorageModule.Root.EnemyResource.Weapon; _currentStrafeTarget = null; _responseTimer = 0; } private void HealthProviderOnResourceDepleted() { ChangeState(EnemyState.Dead); } private void PlayerDetectionOnPlayerOutOfRange() { StateMachine.SetState(EnemyState.Alert); } public override void ExitState() { base.ExitState(); PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange; DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted; _currentStrafeTarget = null; DamageReceiver.ChangeState(false); } public override void PhysicsProcessState(double delta) { base.PhysicsProcessState(delta); if (PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight()) { // SHOOT Shoot(); if (_strafeSpeed > 0) { // Check if a strafe position is needed if (!_currentStrafeTarget.HasValue || NavigationModule.IsNavigationFinished()) { if (_responseTimer < StorageModule.EnemyData.ResponseTime) { _responseTimer += delta; } else { _currentStrafeTarget = CalculateStrafePosition(); _responseTimer = 0; } } } } else { StateMachine.SetState(EnemyState.Alert); return; } if (_currentStrafeTarget.HasValue) { NavigationModule.SetTarget(_currentStrafeTarget.Value); NavigationModule.Move(_strafeSpeed); } } 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(StorageModule.EnemyData.MinStrafeDistance, StorageModule.EnemyData.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(StorageModule.EnemyData.MinStrafeDistance, StorageModule.EnemyData.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 Vector2 GetShootDirection() { if (StorageModule.EnemyData.PredictPlayer && PlayerDetection.LastKnowPlayerVelocity.HasValue) { var predictedDirection = MathFunctions.PredictInterceptPosition(MainObject.GlobalPosition, PlayerDetection.LastKnownPlayerPosition.Value, PlayerDetection.LastKnowPlayerVelocity.Value, EquippedWeapon.WeaponData.BulletData.BulletSpeed); if (predictedDirection.HasValue) return (predictedDirection.Value - MainObject.GlobalPosition).Normalized(); } return ( PlayerDetection.LastKnownPlayerPosition.Value - MainObject.GlobalPosition).Normalized(); } private void Shoot() { if (EquippedWeapon == null) return; if (!PlayerDetection.LastKnownPlayerPosition.HasValue) return; var direction = GetShootDirection(); // Shoot at the player's last known position EquippedWeapon.ShootDirection = direction; //StorageModule.FacingDirection = direction; StorageModule.FacingDirection = direction.SnapToCardinal().Normalized(); StorageModule.AimingDirection = StorageModule.FacingDirection; EquippedWeapon.Shoot(); } }