using Godot; using Cirno.Scripts; using Cirno.Scripts.Components; using System.Collections.Generic; using System.Linq; using Cirno.Scripts.Components.Actors; public partial class EnemyNavigationMovement : MovementHandler { public override Vector2 FacingDirection { get => _parent.FacingDirection; set => _parent.FacingDirection = value; } public override Vector2 MovementDirection { get => _parent.MovementDirection; set => _parent.MovementDirection = value; } public bool IsDestroyed => _parent.IsDestroyed; [Export] private bool _navigationEnabled = false; [Export] public float AlarmReactRange = 200f; [Export] public float PlayerDisengageRange = 500f; [Export(PropertyHint.Layers2DPhysics)] public uint CollisionMask { get; set; } [Export] public Weapon EquippedWeapon; public bool NavigationEnabled { get => _actorAi is not null && _actorAi.Ai is AiState.Enabled && _navigationEnabled && _navigationAgent != null; set => _navigationEnabled = value; } [Export] private PlayerDetection _playerDetection; private ActorAi _actorAi; private NavigationAgent2D _navigationAgent; private AlarmManager _alarmManager; private Vector2? _lastPlayerPosition = null; private bool IsPlayerInRange => _playerDetection is { IsPlayerInRange: true }; private bool IsPlayerInSight => _playerDetection is not null && _playerDetection.IsPlayerInSight(CollisionMask); public override void Init(Actor parent) { base.Init(parent); MovementDirection = Vector2.Zero; FacingDirection = Vector2.Down; _actorAi = parent.GetNode("ActorAi"); _navigationAgent = _parent.GetNodeOrNull("NavigationAgent2D"); _alarmManager = this.GetAlarmManager(); if (_alarmManager != null) { _alarmManager.AlarmEnabled += AlarmManagerOnAlarmEnabled; } } public override void Update(double delta) { } private void AlarmManagerOnAlarmEnabled(Vector2 location) { if (NavigationEnabled && location.DistanceTo(_parent.GlobalPosition) <= AlarmReactRange) { GD.Print($"Enemy {Name} alerted"); _actorAi.State = EnemyState.Alert; _lastPlayerPosition = location; } } public override void PhysicsUpdate(double delta) { if (IsDestroyed) return; if (_actorAi.Ai is not AiState.Enabled) return; switch (_actorAi.State) { case EnemyState.Idle: if (_playerDetection != null && IsPlayerInSight) { _actorAi.State = EnemyState.Alert; GD.Print("Switching to alert"); } break; case EnemyState.Alert: // Update last known player position if it's in range if (IsPlayerInRange) { _lastPlayerPosition = _playerDetection.CachedPlayer.GlobalPosition; } if (NavigationEnabled) { if (_lastPlayerPosition.HasValue) { _navigationAgent.SetTargetPosition(_lastPlayerPosition.Value); } var currentAgentPosition = _parent.GlobalPosition; var nextPathPosition = _navigationAgent.GetNextPathPosition(); var newVelocity = currentAgentPosition.DirectionTo(nextPathPosition) * _parent.MovementSpeed; // Navigation is over, can do other things like shooting if (_navigationAgent.IsNavigationFinished()) { // Shoot player if (IsPlayerInSight) { Shoot(); } // TODO: If player totally left the max range it should stop shooting and go back to idle return; } if (_navigationAgent.AvoidanceEnabled) { _navigationAgent.SetVelocity(newVelocity); } else { _on_navigation_agent_2d_velocity_computed(newVelocity); } _parent.MoveAndSlide(); } else { if (IsPlayerInSight) { Shoot(); } } break; } } private void Shoot() { if (EquippedWeapon == null || !_lastPlayerPosition.HasValue) return; var direction = (_lastPlayerPosition.Value - _parent.GlobalPosition).Normalized(); // Shoot at the player's last known position EquippedWeapon.ShootDirection = direction; FacingDirection = direction; EquippedWeapon.Shoot(); } public void _on_navigation_agent_2d_velocity_computed(Vector2 safeVelocity) { _parent.Velocity = safeVelocity; if (_parent.Velocity.Length() > 0) { _parent.FacingDirection = _parent.Velocity.Normalized(); } } }